2018년 6월 28일 목요일

안드로이드 BLE 기기를 Peripheral(Slave) 모드로 advertising하도록 하기






안드로이드로 BLE(Bluetooth Low Energy) 개발을 하다보면 2대의 폰이 하나는 Peripheral(Slave) 모드로 다른 하나는 Central(Master) 모드로 동작하는 테스트 앱을 만들어야 할 때가 있다.
왜냐하면 Bluetooth Classic(혹은 Bluetooth Basic)과는 달리 BLE의 경우는 Peripheral 모드로 동작하는 기기가 없으면 Central에서 아무리 scan을 해 봐야 검색 자체가 되지 않기 때문이다.
참고로 스마트폰의 경우는 BLE로도 혹은 Bluetooth Classic으로 동작을 하는 '듀얼 모드'(혹은 Bluetooth Smart Ready)를 지원하기 때문에 이상의 작업 환경을 만드는 것이 가능하다.
또 참고로 BLE만을 지원하는 경우를 Bluetooth Smart 디바이스라고 하고 '싱글 모드'로 표현하기도 한다. 이 경우는 기존의 Bluetooth Classic 기기와는 호환되지 않는다.

여기서 BLE가 Peripheral 모드로 동작한다는 뜻은 다른 BLE 디바이스와 connection을 맺기 위해 advertising signal을 주기적으로 내보내는 역할을 하는 기기를 말하고 Central로 동작한다는 뜻은 다른 BLE가 내보내는 advertising signal를 주기적으로 스캔하는 디바이스를 말한다.
아무튼...

이렇게 Peripheral 모드로 동작하는 안드로이드 개발을 하다보면 아래 코드조각의 onStartFailure() 메소드에서 errorCode 1을 반환하는 경우를 만나게 된다.

        AdvertiseCallback advertisingCallback = new AdvertiseCallback(){
          @Override
          public void onStartSuccess(AdvertiseSettings settingsInEffect){
              Toast.makeText(getApplicationContext(), "Advertising 성공~ ", 1).show();
              super.onStartSuccess(settingsInEffect);
          }

          @Override
          public void onStartFailure(int errorCode){
              Log.e("BLE #######", "Advertising onStartFailure: "+errorCode); //여기서 errorCode가 1을 반환하는 에러

              //아래 Toast에서 errorCode가 1이 나오는데 API 문서를 보면
              //ADVERTISE_FAILED_DATA_TOO_LARGE 에러라고 설명이 되어 있다.
              //Failed to start advertising as the advertise data to be broadcasted is larger than 31 bytes.
              Toast.makeText(getApplicationContext(), "Advertising onStartFailure: "+errorCode, 1).show();
              super.onStartFailure(errorCode);
          }
        };

이 경우는 안드로이드 API 문서의 아래 설명과 같이 advertising의 데이터 길이가 31bytes를 넘어서 발생하는 문제이다.

"Failed to start advertising as the advertise data to be broadcasted is larger than 31 bytes."

BLE Peripheral 기기가 내보내는 advertising signal의 길이는 31byte로 한정되어 있는데 코드상에서 설정한 이 데이터의 크기가 31byte를 넘어서 발행한 경우이다.
아래 코드 조각이 advertising 데이터를 설정하는 코드이다.

        BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();

        AdvertiseSettings settings = new AdvertiseSettings.Builder()
                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                .setConnectable(false)
                .build();

        ParcelUuid pUuid = new ParcelUuid(UUID.fromString(getString(R.string.ble_uuid2)));

        AdvertiseData data = new AdvertiseData.Builder()
                .setIncludeDeviceName(true)
                .addServiceUuid(pUuid)
                .addServiceData(pUuid, "Data".getBytes(Charset.forName("UTF-8")))
                .build();

위의 AdvertiseData의 설정을 아래와 같이 수정해본다.

        AdvertiseData data = new AdvertiseData.Builder()
                .setIncludeDeviceName(true)
//                .addServiceUuid(pUuid)
//                .addServiceData(pUuid, "Data".getBytes(Charset.forName("UTF-8")))
                .build();

UUID와 Service Data를 advertising signal 데이터에 포함시키지 않도록 주석처리했다.
이제 문제가 해결되었을 것이고 다른 BLE에서 본 소스를 실행한 BLE를 정상적으로 잘 스캐닝 및 검색을 할수 있을 것이다.

리눅스, 윈도우즈에서 UUID 값 생성하기






블루투스 연동이나 BLE 연동 등의 작업을 하다보면 UUID 값을 필요로 하는 경우가 있다.
이때 UUID를 생성할수 있는 간단한 방법이 있다.
리눅스 시스템이 있거나(혹은 VirtualBox 등으로 리눅스 설치 등) 혹은 윈도우즈 환경에서도 UUID 값을 간단하게 생성할수 있는 방법이 있다.

리눅스의 경우 터미널 창에서 uuidgen 명령어를 실행하면 UUID 값을 생성해 준다. 반드시 root로 로그인하지 않아도 된다.

root@.....# uuidgen
f26e4cff-704a-47b5-9a25-77f7bc9ba54a

윈도우즈의 경우는 DOS 창(커맨더 창, 명령 프롬프트 창)을 띄워서 다음 명령을 수행하면 역시 UUID 값을 얻을수 있다.

Microsoft Windows [Version 10.0.16299.492]
(c) 2017 Microsoft Corporation. All rights reserved.

C:\Users\User>powershell -Command "[guid]::NewGuid().ToString()"
99924071-3058-4acd-bd95-978a77db9352

C:\Users\User>

2018년 6월 20일 수요일

CSS를 이용하여 테이블의 특정 column 보이지 않도록(hidden으로) 처리 하기






CSS를 이용하여 테이블의 특정 column 보이지 않도록(hidden으로) 처리 하기

-----------------------------------------------------
순번 | 이름 | 나이 | 성별 | 주소 | 전화번호 | 기타 |
-----------------------------------------------------
  ........................ 중 략 ......................

위와 같은 테이블이 있다고 할때 코드는 아래와 같은 식이 될 것이다.

      <table>
        <thead>
          <th>순번</th>
          <th>이름</th>
          <th>나이</th>
          <th>성별</th>
          <th>주소</th>
          <th>전화번호</th>
          <th>기타</th>
        </thead>
        <tr>
          <td>순번 정보 표시...</td>
          <td>이름 정보 표시...</td>
          <td>나이 정보 표시...</td>
          <td>성별 정보 표시...</td>
          <td>주소 정보 표시...</td>
          <td>전화번호 정보 표시...</td>
          <td>기타 정보 표시...</td>
        </tr>
      </table>


이럴 경우 나이 항목(column), 나이 칼럼 전체가 보이지 않도록 즉 |순번|이름|성별|주소|... 와같이 보이도록 해야 할 경우 이를 CSS를 이용하면 아주 간단하게 처리가 된다.
즉 나이 칼럼이 hidden으로 처리되게 하는 것이다. 물론 이때 비록 보이지는 않지만 내부적으로는 나이 칼럼이 존재하고 이 값을 form 전송시 전송한다거나 하는 작업은 정상적으로 이뤄지게 된다.
이럴경우 다음과 같이 처리하면된다.

  <head>
      ... 중 략 ...
    <style type="text/css">
      ... 중 략 ...
      th:nth-of-type(3) { display: none; }   
      td:nth-of-type(3) { display: none; }
      ... 중 략 ...
    </style>
  </head>

nth-of-type()의 괄호안에 몇번째 칼럼을 보이지 않게 할지를 지정해 주면된다.

2018년 6월 19일 화요일

웹 페이지에서 테이블(table)의 짝수 행의 색상을 다른 색으로 표시하기





PHP에서 DB에 저장되어 있는 데이터를 가져와서 도표(table) 형태로 표시할 때 모든 행이 같은 색상이면 가독성의 측면에서 바람직하지 않을 것이다. 이럴때 홀수 행의 색상을 약간 다르게 표시하면 가독성이 훨씬 높아 질 것이다.
이럴 경우 CSS를 이용하면 매우 간단하게 처리를 할수가 있다.
아래와 같은 코드가 있다면 이 코드가 생성해 내는 테이블은 짝수 행의 색상이 약간 짙은 회색(#f2f2f2)으로 표현되어 한 행 건너마다 기본 색상과 다르게 표현되어 가독성이 훨신 좋아 질 것이다.
이러한 기능을 하는 핵심 코드가 아래에서 CSS의 #tmTable tr:nth-child(even){background-color: #f2f2f2;}이다.
물로 이를 위해서는 table의 id를 tmTable로 지정해 주어야 하는 것은 당연한 일이다.

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>TM현황</title>
    <style type="text/css">
     ... 중 략 ...
      #tmTable tr:nth-child(even){background-color: #f2f2f2;}
    </style>
  </head>
  <body>
    <div id="wrap">
      <h1>업체현황</h1>
      <a href="./login.html">로그인</a>
      <table id="tmTable">
        <thead>
          <th>No.</th>
          <th>날짜</th>
          <th>업체명</th>
          ... 중 략 ...
          <th>복합기</th>
        </thead>
     <?php
include_once 'config.php';
$conn = mysqli_connect(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if(!$conn){
echo "Error: Unable to connec to MySQL.".PHP_EOL;
echo "Debugging errno: ".mysqli_connect_errno().PHP_EOL;
echo "Debugging error: ".mysqli_connect_error().PHP_EOL;
exit;
}

if(mysqli_connect_errno()){
echo "Failed to connect to TM Database".mysqli_connect_error();
exit;
}

$query = "SELECT * FROM tm order by seq desc;";
$rt = mysqli_query($conn, $query);

while($row = mysqli_fetch_assoc($rt))
{
         echo "<tr>";
           echo "<td>".$row['seq']."</td>";
           echo "<td>".$row['year'].".".$row['month'].".".$row['day']."</td>";
           echo "<td>".$row['company']."</td>";
                            ... 중 략 ...
echo "<td>".$row['copier']."</td>";
        echo "</tr>";
}

  mysqli_close($conn);
     ?>
      </table>
    </div>
  </body>
</html>

PHP에서 strtotime 함수와 date 함수를 이용한 타임스탬프 변환하기






PHP로 웹 프로그래밍시 "미팅 일시"를 기준으로 DB의 정보를 정렬해서 웹에 보여준다고 가정할때 어떤 식으로 처리하면 좋을까?
예를 들어 미팅 일시는 2018.6.19 15:30과 같은 시간이라고 가정해 보자. DB의 특정 필드에 이 값 그대로를 저장한다면 추후에 미팅 일시 필드를 기준으로 오름차순이나 내림 차순으로 정렬하기가 불편해 지게 된다.
이럴 경우에 '타임스탬프'를 이용하면 모든 것이 깔끔해진다.

타임스탬프란 1970년 1월 1일 0시 0분 0초를 기점으로 총 경과한 시간을 초 단위로 표현한 정수 값이다.
가령 2018.6.19 4:28분 정도의 시간이면(초는 귀찮으니 그냥 생략) 타임스탬프로 1529393299 정도의 값이 나올 것이다.

정리하면
 -. "미팅 일시"를 사람이 입력할때는 2018.6.19 15:30과 같은 형식으로 입력
 -. 2018.6.19 15:30의 시간을 DB에 저장할 때는 이에 해당하는 타임스탬프로 저장
 -. DB에 저장되어 있는 "미팅 일시" 필드의 타임스탬프를 기준으로 정렬 후 웹 페이지에 보여줄 때는 타임스탬프를 "년.월.일 시:분"과 같은 형태로 표시

결국은 이 과정을 위해서는 
타임스탬프 시간 ↔ 년.월.일 시:분
의 시간 형태로 상호 변환을 처리하는 과정이 필요하게 되어진다.

이를 위한 함수를 PHP에서는 제공해 주고 있다. 그 함수가
date()
strtotime() 두 함수이다.

년.월.일 시:분의 값을 타임스탬프로 변환해 보자. 
여기서 참고적으로 "1970.01.1 13:22"과 같은 형태는 strtotime() 함수가 인식하지 못하는 형태이다. 인식할수 있는 형태는
1970-01-01 13:22 혹은
1970/01/01 13:22과 같은 형태라야 한다.

$meetingTime = '1970-01-1 13:22';
echo "1970년 1월 1일 13:22을 타임스탬프 값으로 : ".strtotime($meetingTime)."<br/>";

1970년 1월 1일 13:22의 타임스탬프 값은 15720이다.
이번에는 타임스탬프의 값을 년.월.일 시:분의 형태로 변환해 보자.

$mTimeStamp = '15720';
echo "타임스탬프의 값을 한국형 시간형태로 변환 : ".date('Y.m.d H:i', $mTimeStamp)."<br/>";


2018년 6월 10일 일요일

textarea 내용을 초기화 하는 법





textarea 내용을 초기화 하는 법

textarea의 내용을 초기화 상태가 되고 "메시지를 입력하세요"와 같은 힌트 글이 희미하게 보이도록 하는 방법에 대한 것이다.

<html>
... 중략 ...

<body>
<form action="push_noti.php" method="post">
<textarea id="mTxtArea" onclick=this.value='';" name="message" rows="4" cols="50" placeholder="메시지를 입력하세요" required>
</textarea><br/>
<input type="submit" name="submit" value="Send" id="submitButton">
</form>
<script type="text/javascript">
document.getElementById("mTxtArea").value='';
</script>
</body> 
</html>

여기서 구현한 방법은 textarea에 id 값을 지정해서 JavaScript에서 이 id 값을 이용해서 textarea의 내용을 초기화 했다는 것이다.
JavaScript에서 textarea을 초기화한 핵심 코드는 아래 코드이다.

document.getElementById("mTxtArea").value='';

그리고 textarea를 클릭했을 때 onclick 이벤트를 활용해서 역시 내용을 초기화 하기도 했다.
   
placeholder="메시지를 입력하세요"는 textarea에 아무런 값도 없을 때 힌트 메시지를 보이게 하는 기능이다.

2018년 6월 6일 수요일

FCM 안드로이드 앱 개발시 error: cannot access zzbgl, class file for com.google.android.gms.internal.zzbgl not found 에러 해법





FCM 안드로이드 앱 개발시 error: cannot access zzbgl, class file for com.google.android.gms.internal.zzbgl not found 에러 해법

Firebase Cloud Messaging 안드로이드 개발시 아래와 같은 에러를 만나는 경우가 있다.
이 에러는 라이브러리로 인한 에러인데 FCM에서 library로 인한 에러의 경우 컴파일 단계에서 원천적으로 되지 않는 경우가 발생한다.
남의 라이브러리를 사용할때마다 늘상 씨름을 한판하지 않고는 쉽게 넘어가지를 않는 것 같다.
com.google.firebase.messaging.FirebaseMessagingService를 상속받은 MyFirebaseMessagingService 클래스에서 아래와 같은 에러가 발생하면서 컴파일이 되지를 않는다.

D:\ExFCMTest\app\src\main\java\com\example\joe\exfcmtest\MyFirebaseMessagingService.java:29: error: cannot access zzbgl
        Map<String, String> data = remoteMessage.getData();
                                                ^
  class file for com.google.android.gms.internal.zzbgl not found
Note: D:\ExFCMTest\app\src\main\java\com\example\joe\exfcmtest\MyFirebaseMessagingService.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
1 error
:app:compileDebugJavaWithJavac FAILED
:app:buildInfoGeneratorDebug

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s
18 actionable tasks: 7 executed, 11 up-to-date


이 에러가 발생했을 때의 앱 수준의 build.gradle(app/build.gradle)에 있는 dependencies의 내용이다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    compile 'com.google.firebase:firebase-core:16.0.0'
    compile 'com.google.firebase:firebase-messaging:12.0.1'
}

이 문제는 앱 수준의 build.gradle의 dependencies의  

compile 'com.google.firebase:firebase-core:16.0.0'를 

compile 'com.google.firebase:firebase-core:11.8.0' 

로 바꾸었더니 정상적으로 compile이 되었다. 그래서 dependencies가 아래와 같은 모양을 이루었다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    compile 'com.google.firebase:firebase-core:11.8.0'
    compile 'com.google.firebase:firebase-messaging:12.0.1'
}

이제 컴파일이 되어 Firebase Console 창에서 푸시 메시지를 해당 앱으로 날리니 이번에는 앱이 강제 종료가 된다. 아래와 같은 에러 메시지를 뿜으면서

06-06 18:40:14.223 13813-16293/com.example.joe.exfcmtest E/AndroidRuntime: FATAL EXCEPTION: pool-1-thread-1
    Process: com.example.joe.exfcmtest, PID: 13813
    java.lang.NoSuchMethodError: No static method zzamg()Lcom/google/android/gms/common/util/zzd; in class Lcom/google/android/gms/common/util/zzh; or its super classes (declaration of 'com.google.android.gms.common.util.zzh' appears in /data/app/com.example.joe.exfcmtest-1/split_lib_dependencies_apk.apk)
        at com.google.android.gms.internal.zzcim.<init>(Unknown Source)
        at com.google.android.gms.internal.zzcim.zzdx(Unknown Source)
        at com.google.android.gms.measurement.AppMeasurement.getInstance(Unknown Source)
        at com.google.firebase.messaging.zzd.zzde(Unknown Source)
        at com.google.firebase.messaging.zzd.zzc(Unknown Source)
        at com.google.firebase.messaging.zzd.zzf(Unknown Source)
        at com.google.firebase.messaging.FirebaseMessagingService.handleIntent(Unknown Source)
        at com.google.firebase.iid.zzc.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
        at java.lang.Thread.run(Thread.java:762)
06-06 18:40:14.287 13813-13813/com.example.joe.exfcmtest D/ViewRootImpl@c3f2698[MainActivity]: mHardwareRenderer.destroy()#4
    dispatchDetachedFromWindow
06-06 18:40:14.360 13813-13813/com.example.joe.exfcmtest D/InputTransport: Input channel destroyed: fd=70

이 문제는 라이브러리들의 버전이 일치하지 않아서 발생하는 문제이다. 위의 dependencies를 보면 아래와 같이 11.8.0과 12.0.1이 혼재되어 있다.

    compile 'com.google.firebase:firebase-core:11.8.0'
    compile 'com.google.firebase:firebase-messaging:12.0.1'

따라서     

compile 'com.google.firebase:firebase-messaging:12.0.1'를     

compile 'com.google.firebase:firebase-messaging:11.8.0'

으로 바꾸었다.
바꾸는건 12.0.1을 11.8.0으로 바꾸어서 타이핑해 주면 된다.

그래서 dependencies가 아래와 같이 되었다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
//    compile 'com.google.firebase:firebase-core:16.0.0' //error: cannot access zzbfm
    compile 'com.google.firebase:firebase-core:11.8.0' //O.K.
    compile 'com.google.firebase:firebase-messaging:11.8.0'
}

이제 컴파일도 정상적으로 되고 Firebase Console창에서 푸시 메시지를 날리면 정상적으로 잘 수신을 한다.
참고로 루트 수준의 build.gradle에 다음과 같이 구글의 maven 저장소를 추가해 준다.

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url "https://maven.google.com" //구글의 Maven repository
        }
    }
}

아래 사이트가 도움이 된다.

https://firebase.google.com/docs/android/setup?authuser=0#manually_add_firebase


2018년 6월 5일 화요일

FCM(Firebase Cloud Messaging) 안드로이드 앱을 만드는 중에 is not assignable to 'android.app.Service' Validates resource references inside Android XML files 에러 해법





FCM 안드로이드 앱을 만드는 중에 아래와 같은 에러가 발생하는 원인과 해결책

가정하기를 
 -. 패키지명 : com.example.joe.exfcmtest
 -. com.google.firebase.messaging.FirebaseMessagingService를 상속받을 클래스가 MyFirebaseMessagingService라고 할때

이러한 상황가운데서 아래와 같은 에러를 만난다면 

'com.example.joe.exfcmtest.MyFirebaseMessagingService' is not assignable to 'android.app.Service' less... (Ctrl+F1) 
Validates resource references inside Android XML files.

앱 수준의 build.gradle(프로젝트 수준의 build.gradle이 아님)에서 필요한 아래의 dependencies가 빠져 있어서의 문제다.

compile 'com.google.firebase:firebase-messaging:12.0.1'  (버전은 현재의 최신 버전과 다를수 있음)

이 문제는 참으로 황당하게도 구글의 Firebase SDK 추가에 대한 설명 자체에서 빼먹고 제시해 주지 않음으로 인해 만나게되는 황당한 에러이다.
Firebase 콘솔에서 제시한 대로 따라하다보면 아래 이미지에서 보듯이 compile 'com.google.firebase:firebase-messaging:12.0.1'를 포함시키라는 설명이 전혀 없다. 나쁜...



이로인해 Manifest 파일의 com.google.firebase.messaging.FirebaseMessagingService를 상속받을 Service 클래스에 대해 아래 그림과 같은 에러가 발생한다.

        <service android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>



그리고 다음 에러



이 문제를 해결할려면 아래 그림에서 보는바와 같이 앱 수준의 build.gradle



compile 'com.google.firebase:firebase-messaging:12.0.1'를 dependencies에 포함시켜야 된다.



안드로이드는 너무 자주, 많이 바뀌는 통에...