# Television - Smart Surveillance Camera with Telegram
카메라로 이미지를 캡쳐하여 분석 후 분석 결과와 이미지를 IoTjs 웹서버를 통해 전달하고, 추가로 텔레그램 봇과 연동하는 프로젝트 입니다.
:::hw
Board|Rpi3 or Tizen Compatible board|1|Single Board Computer
Sensor|USB Camera|1|본 강의는 SPC-A1200MB를 사용하였습니다. 다른 USB카메라도 가능합니다.
:::
:::sw
OS|Tizen|Tizen 5.0
App|Smart Surveillance Camera Application|본 프로젝트의 소스코드는 다음 위치 `television` 브랜치에 공개되어 있습니다 `git://git.tizen.org/apps/native/smart-surveillance-camera`
Etc.|IoT.js|Tizen 5.0에 포함되어 있지만, Web socket을 사용하기 위해서는 별도로 빌드한 패키지가 필요합니다
:::
# 프로젝트 구조와 동작 Overview
본 프로젝트는 Tizen의 Camera API, Vision API, IoT.js와 오픈소스 메신저 텔레그램을 이용해서 Smart Surveillance Camera를 만드는 방법을 소개합니다.
본 프로젝트는 2개의 Application으로 구성되어 있습니다.
**카메라로 이미지를 확보하여 분석 후 이미지 파일을 생성하여 텔레그램으로 전송하는 Surveillance Camera Application**과 **Surveillance Camera Application에서 저장한 이미지를 웹 브라우저로 볼 수 있도록 제공하는 Web Server Application** 입니다.

두 어플리케이션은 모두 `Service Application`이며, 각각의 어플리케이션은 **독립적으로 동작**하므로, 필요에 따라 구성을 변경 할 수 있으며, 각각 설치하고, 실행시켜야 합니다.
**타이젠에서 카메라를 활용하는 방법과, 디바이스 내에서 자체적으로 이미지 검사한 후 그 결과를 웹 서버나 외부 서비스를 통해 외부에 전달하는 방법에 대한 완전히 동작하는 시나리오 샘플입니다.**
# 카메라로 이미지 다루기
타이젠에서 카메라를 활용하는 방법에 대해서 알아보겠습니다.
타이젠에서 제공하는 편리한 API들을 이용하여 카메라를 컨트롤하고, 카메라를 통해 얻은 이미지 데이터를 처리하는 방법을 확인 할 수 있습니다.

### 기억해야할 중요한 점
1. preview 콜백은 다른 쓰레드에서 실행된다
1. preview 이미지 데이터와 encoding 된 이미지 데이터는 다르다
## 카메라 시작하기
카메라를 컨트롤 하기 위해서 우선 타이젠의 카메라에 대해서 알아야 합니다.

위와 같은 상태를 갖으며, 상태에 따라 전환될 수 있는 상태가 제한되어 있으니 잘 알아두어야 합니다.
### 가장 먼저 해줘야 할 일
타이젠 카메라를 사용하기 위해서는 `Privilege`를 추가한 다음,
```xml
<privilege>http://tizen.org/privilege/camera</privilege>
```
카메라 헤더를 추가하고!
```c
#include <camera.h>
```
카메라 핸들을 만들어서 사용합니다. 간단하쥬?
```c
camera_create(CAMERA_DEVICE_CAMERA0, &(camera_data->cam_handle));
```
### 기본적인 카메라 API
기본적으로 핸들을 만들고, 이미지의 품질이나 해상도 등을 설정 합니다. 그리고 프리뷰나 포커스, 상태 변경 이벤트 등에 콜백 함수를 설정합니다. 설정이 완료 된 후에는 필요한 시점에 프리뷰 또는 캡쳐를 동작하도록 하면 됩니다. 와우, 정말 쉽네요!
제가 사용한 API들은 아래와 같습니다.
```c
//설정
camera_create
camera_attr_set_image_quality
camera_set_preview_resolution
camera_set_capture_resolution
camera_set_capture_format
camera_set_state_changed_cb
camera_set_preview_cb
camera_set_focus_changed_cb
camera_attr_foreach_supported_af_mode
//사진 찍기
camera_start_preview
camera_start_focusing
camera_stop_preview
camera_get_state
camera_start_capture
//종료
camera_destroy
```
### 프로젝트에서 재구성된 카메라 API
카메라 API들을 이용해서 프로젝트에서 사용하기 좋게 resource_camera.c 를 만들어 보았습니다.
```c
int resource_camera_init(preview_image_buffer_created_cb preview_image_buffer_created_cb, void *user_data);
int resource_camera_start_preview(void);
int resource_camera_capture(capture_completed_cb capture_completed_cb, void *data);
void resource_camera_close(void);
```
`resource_camera_init`에서는 프로젝트에서 사용하게 될 설정값을 세팅하고, preview 콜백을 받아서 저장해둡니다.
`resource_camera_start_preview`는 실제로 preview를 시작하도록 트리거 하는 역할입니다.
`resource_camera_capture`는 이번 프로젝트에서 사용하지는 않습니다. 사진을 파일로 남기고 싶을때 콜백과 함께 호출해서 사용하면됩니다.
`resource_camera_close` 에서는 사용된 자원들을 해제하고 뒷정리합니다.
타이젠 카메라. 정말 쉽네요!
## Preview는 다른 쓰레드!
카메라를 다루면서 주의할 점이 하나 있습니다. 카메라의 Preview 콜백은 어플리케이션의 쓰레드가 아닌 카메라API에서 생성한 쓰레드에서 실행이 됩니다.
```c
/**
* @brief Registers a callback function to be called once per frame when previewing.
* @since_tizen @if MOBILE 2.3 @elseif WEARABLE 2.3.1 @endif
* @remarks This callback does not work in the video recorder mode.n
* Before 4.0, the only allowed state for calling this function was #CAMERA_STATE_CREATED.n
* Since 4.0, #CAMERA_STATE_PREVIEW has been added as an allowed state,n
* so that this function could be called before previewing or even while previewing.n
* A registered callback is called on the internal thread of the camera.n
* A video frame can be retrieved using a registered callback,n
* and the buffer is only available in a registered callback.n
```
### ecore thread의 활용
Application 개발에 멀티 쓰레드 프로그래밍은 필수! 멀티 쓰레드 프로그래밍엔 **Thread Safety**가 필수!
관련해서 Tizen에서는 ecore thread API를 제공하고 있습니다. 아래 링크에 잘 정리된 가이드가 있습니다.
[Using Threads - Ecore](https://developer.tizen.org/development/guides/native-application/user-interface/efl/core-loop-and-os-interfacing/using-threads?langredirect=1)
>Ecore offers a simplified API for managing threads in EFL applications. The Ecore API applies to a typical scenario where the main thread creates another thread, which in turn sends data back to the main thread or calls GUI-related functions. GUI-related functions are not thread-safe.
본 프로젝트의 카메라에서는 preview 콜백 쓰레드에서 main 쓰레드로 전환을 위해 아래 API를 사용하였습니다.
```c
/**
* @brief Calls callback asynchronously in the main loop.
* @since 1.1.0
*
* @param callback The callback to call in the main loop
* @param data The data to give to that call back
*
* For all calls that need to happen in the main loop (most EFL functions do),
* this helper function provides the infrastructure needed to do it safely
* by avoiding dead lock, race condition and properly wake up the main loop.
* ...
*/
EAPI void ecore_main_loop_thread_safe_call_async(Ecore_Cb callback, void *data);
```
## Preview 이미지 다루기
본 프로젝트에서는 Camera에서 연속된 Preview 이미지를 추출하여 사용합니다. **Preview Event가 모든 이벤트의 시작점**이 됩니다.
`camera_start_preview`로 `previewing` 상태가 되면 설정된 `fps`로 preview callback이 호출됩니다. 해당 callback은 `fps` 영향을 받기 때문에 최대한 짧게 종료되는 것이 좋습니다.
이미지 분석을 하기위해 사용될 **Media Vision API**에서 사용할 수 있는 형태의 이미지 버퍼를 생성한 후 해당 버퍼를 전달해 주는 역할만을 수행하도록 아래와 같이 구현하였습니다.
```c
static void __preview_image_buffer_created_cb(void *data)
{
//...생략
image_colorspace = __convert_colorspace_from_cam_to_mv(image_buffer->format);
goto_if(image_colorspace == MEDIA_VISION_COLORSPACE_INVALID, FREE_ALL_BUFFER);
__copy_image_buffer(image_buffer, ad);
source = controller_mv_create_source(image_buffer->buffer, image_buffer->buffer_size, image_buffer->image_width, image_buffer->image_height, image_colorspace);
//...생략
if (source)
controller_mv_push_source(source);
//...생략
pthread_mutex_lock(&ad->mutex);
if (!ad->image_writter_thread) {
ad->image_writter_thread = ecore_thread_run(__thread_write_image_file,
__thread_write_image_file_end_cb,
__thread_write_image_file_cancel_cb,
ad);
} else {
_E("Thread is running NOW");
}
pthread_mutex_unlock(&ad->mutex);
```
Preview Callback이 카메라 FW에서 생성된 쓰레드에서 실행되므로 이후 동작을 main loop에서 실행하기 위해서 `ecore_main_loop_thread_safe_call_async`을 사용했습니다.
### 이미지 파일로 저장
Preview에 들어오는 데이터는 `NV12, NV12T, NV16, NV21, YUYV, UYVY, and YUV420P` 형식의 pixel format 입니다. 이것을 흔히 사용하는 `jpg` 형태로 변경하기 위해서는 인코딩 과정을 거쳐야 합니다.
**내가 다루고 있는 이미지 데이터가 어떤 포맷의 데이터인지 명확히 알고 있어야 합니다.** preview 에서 받은 데이터를 바로 파일로 쓰거나 전송하면 제대로 처리되지 않을 수 있습니다.
인코딩은 Tizen에서 제공하는 Image Util API를 통해서 직접 할 수도 있고, Tizen Camera API인 `camera_start_capture`를 이용해서 할 수도 있습니다. 본 프로젝트에서는 Image Util API를 사용하고 있습니다만, resource_camera.c 에 구현된 `resource_camera_capture`는 `camera_start_capture`를 사용하였으니 필요한 경우에 사용하셔도 됩니다.
## USB Camera 설정 관련 문제
USB 카메라를 사용할 경우 제품이 지원하는 상세 명세와 플랫폼의 정보와 다를 경우 동작 하지 않을 수 있습니다. 예를 들어 해상도 설정이 맞지 않는 경우 API에서 에러를 리턴하게 되는데, 아래 내용을 참고하셔서 설정 파일을 변경하면 쉽게 해결 됩니다.
또는 밝기의 설정 값등의 문제가 발생 할 수 있는데, 아래 커맨드는 ini 설정 값에서 `Brightness` 설정을 주석처리하여서 카메라가 가지고 있는 기본값을 사용하도록 처리한 것입니다.
```bash
mount -o remount rw /
sed -i 's/^Brightness/;Brightness/g' /etc/multimedia/mmfw_camcorder_camera0.ini
sync
reboot
```
H/W 구성에 따른 포팅 문제는 아래 포팅 가이드를 참고하시고, 막히는 부분은 타이젠 스페셜리스트를 찾아주세요!
[타이젠 포팅 가이드](https://docs.tizen.org/platform/porting/multimedia#configuration)
# Media Vision API로 움직임 검출하기
Preview에서 확보한 이미지 데이터를 분석해서 움직임을 검출할 수 있습니다. 단순히 이미지를 추출하는 것이 아니라 1차적으로 이미지를 검사할 수 있다는 것은 여러가지 의미가 있습니다.
1. 네트워크, 컴퓨팅 자원 효율을 높일 수 있다.
1. 보안을 강화 할 수 있다.
1. 데이터 재가공을 통한 새로운 시나리오 발굴가능
이미지를 분석하는 여러가지 방법이 있지만, 여기서는 타이젠에서 제공하는 Media Vision API만을 사용합니다. 이미지를 분석하는 이 단계에서는 **프로젝트의 목적과 목표에 맞게 API를 선택하여 잘 사용하는 것이 가장 중요합니다.**

## Media Vision 소개
Tizen의 Media Vision API는 다양한 기능을 제공하는데, 본 프로젝트에서는 [Media Vision Surveillance](https://developer.tizen.org/development/api-references/native-application?redirect=https://developer.tizen.org/dev-guide/5.0.0/org.tizen.native.mobile.apireference/group__CAPI__MEDIA__VISION__MODULE.html)기능을 사용해서 연속된 이미지 소스에서 움직임을 검출하였습니다.

> Media Vision Surveillance provides functionality can be utilized for creation of video surveillance systems. The main idea underlying surveillance is event subscription model.
필요한 기능에 대해서는 적용해서 테스트해 보고 용도에 맞게 사용하는 것이 중요합니다.
## Media Vision 설정
Media Vision API를 사용하기전에 어떤 소스를 어떻게 분석할지, 분석 결과를 어떻게 전달 받을지 등에 대해서 설정이 필요합니다. 일종의 분석 틀을 만들어 놓는 과정이라고 생각하면 됩니다.
본 프로젝트에서는 `controller_mv.c`의 `controller_mv_set_movement_detection_event_cb` 함수로 구현이 되어 있습니다.
중요한 부분은 어떤 내용을 분석할 지 설정하는 부분과 검출 이벤트가 발생하였을 경우 전달 받을 callback 등을 설정하는 부분입니다.
```c
mv_engine_config_set_int_attribute(engine_cfg, MV_SURVEILLANCE_MOVEMENT_DETECTION_THRESHOLD, 50); /* 10 is default value [0 ~ 255] */
ret = mv_surveillance_event_trigger_create(MV_SURVEILLANCE_EVENT_TYPE_MOVEMENT_DETECTED, &mv_data->mv_trigger_handle);
if (ret) {
_E("failed to mv_surveillance_event_trigger_create - [%s]", __mv_err_to_str(ret));
goto ERROR;
}
ret = mv_surveillance_subscribe_event_trigger(mv_data->mv_trigger_handle, VIDEO_STREAM_ID, engine_cfg, __movement_detected_event_cb, mv_data);
```
## Media Vision 이미지 소스 주입
앞의 설정 과정에서 틀을 만들었다면, 그 다음 할 일은 카메라의 Preview callback으로부터 전달 받은 이미지 소스를 그 틀에 집어 넣는 일이 필요합니다.
아래의 callback 함수에서 `controller_mv_create_source`로 Media Vision에 맞는 이미지 소스를 생성한 다음 `controller_mv_push_source`로 이미지 소스를 집어넣고 있는 것을 볼 수 있습니다.
```c
static void __preview_image_buffer_created_cb(void *data)
{
//...생략
image_colorspace = __convert_colorspace_from_cam_to_mv(image_buffer->format);
goto_if(image_colorspace == MEDIA_VISION_COLORSPACE_INVALID, FREE_ALL_BUFFER);
__copy_image_buffer(image_buffer, ad);
source = controller_mv_create_source(image_buffer->buffer, image_buffer->buffer_size, image_buffer->image_width, image_buffer->image_height, image_colorspace);
//...생략
if (source)
controller_mv_push_source(source);
```
특이한 점은 `controller_mv_push_source`로 **이미지 소스를 집어 넣고, 움직임이 검출될 경우 곧 바로 등록된 콜백이 호출되는 `Blocking API`라는 점 입니다.** 즉, `controller_mv_push_source`아래 코드는 vision에 등록된 콜백이 호출된 이후 수행이 됩니다.
## Media Vision 검출 결과 처리
틀을 만들고, 이미지 소스를 집어 넣고 나서는 분석 결과를 기다리면 됩니다. 분석 결과 움직임이 검출되지 않을 경우 callback은 호출되지 않습니다.
Vision API에서 넘어온 검출 결과를 바탕으로 움직임의 총 면적과 움직임 영역 정보를 상대값으로 변경한 검출 데이터를 생성하는 부분입니다.
하나의 움직임은 8개 숫자로 구성되며 `xxyywwhh`의 값을 갖도록 하였습니다.
- xx는 0~99까지의 값을 갖는 x 상대 좌표
- yy는 0~99까지의 값을 갖는 y 상대 좌표
- ww는 0~99까지의 값을 갖는 상대 넓이
- hh는 0~99까지의 값을 갖는 상대 높이
```c
static void __movement_detected_event_cb(mv_surveillance_event_trigger_h trigger, mv_source_h source, int video_stream_id, mv_surveillance_result_h event_result, void *data)
//...생략
ret = mv_surveillance_get_result_value(event_result, MV_SURVEILLANCE_MOVEMENT_NUMBER_OF_REGIONS, &move_regions_num);
retm_if(ret, "failed to mv_surveillance_get_result_value for %s - [%s]", MV_SURVEILLANCE_MOVEMENT_NUMBER_OF_REGIONS, __mv_err_to_str(ret));
regions = malloc(sizeof(mv_rectangle_s) * move_regions_num);
ret = mv_surveillance_get_result_value(event_result, MV_SURVEILLANCE_MOVEMENT_REGIONS, regions);
retm_if(ret, "failed to mv_surveillance_get_result_value for %s - [%s]", MV_SURVEILLANCE_MOVEMENT_REGIONS, __mv_err_to_str(ret));
for (i = 0; i < move_regions_num; i++) {
if (regions[i].width * regions[i].height < THRESHOLD_SIZE_REGION || result_count >= MV_RESULT_COUNT_MAX)
continue;
result[result_index] = regions[i].point.x * 99 / IMAGE_WIDTH;
result[result_index + 1] = regions[i].point.y * 99 / IMAGE_HEIGHT;
result[result_index + 2] = regions[i].width * 99 / IMAGE_WIDTH;
result[result_index + 3] = regions[i].height * 99 / IMAGE_HEIGHT;
result_count++;
result_index = result_count * 4;
valid_area_sum += regions[i].width * regions[i].height;
}
free(regions);
mv_data->movement_detected_cb(valid_area_sum, result, result_count, mv_data->movement_detected_cb_data);
}
```
위에서 아래의 콜백 함수가 호출되며, 콜백 함수 내에서 결과를 정리해서 **텔레그램 메시지도 보내고, Exif에 움직임 정보도 저장합니다.**
```c
static void __mv_detection_event_cb(int area_sum, int result[], int result_count, void *user_data)
//...생략
if (now < ad->last_valid_event_time + VALID_EVENT_INTERVAL_MS) {
ad->valid_event_count++;
} else {
ad->valid_event_count = 1;
}
ad->last_valid_event_time = now;
if (ad->valid_event_count < THRESHOLD_VALID_EVENT_COUNT) {
__set_result_info(result, result_count, ad, 0);
return;
}
int ratio = (double) area_sum * 100 / (double) IMAGE_RESOLUTION;
_D("area_sum [%d], ratio [%d]", area_sum, ratio);
char* msg = g_strdup_printf("Motion Detected! %d%% %d zones", ratio, result_count);
__send_telegram_message(msg, ad);
free(msg);
ad->valid_event_count = 0;
__set_result_info(result, result_count, ad, 1);
```
# 분석 결과를 exif에 넣어 jpg 생성하기
분석 결과를 이미지와 함께 전달하는 방법으로 Exif 정보에 분석결과를 넣어서 `jpg`로 인코딩 하는 방법을 선택했습니다. 이 과정에서 **중요한 점은 Exif나 jpg를 사용했다는 것이 아니라 분석 결과를 바탕으로 상황에 맞는 정보를 생성할 수 있다는 점입니다.**

다양한 방법이 있지만, 본 프로젝트에서는 `Exif`를 위해서는 `libexif`를 사용하였고, `jpg` 인코딩을 위해서는 타이젠 Image Util API를 사용하였습니다.
이미지 파일은 IoTjs Web Server Application과 함께 사용하기 위해 `shared folder`에 저장하였습니다.
[File System Directory Hierarchy](https://developer.tizen.org/development/training/native-application/understanding-tizen-programming/file-system-directory-hierarchy?langredirect=1)
>Used to share resources with other applications. The resource files are delivered with the application package.
To get this directory path of your own application, call the app_get_shared_resource_path() function of the App common module.
## 인코딩과 파일 생성은 쓰레드 사용
파일 생성은 별도의 쓰레드에서 처리하도록 하였습니다. 파일 생성은 시간이 많이 소모되는 작업이므로 별도의 쓰레드를 사용하고, **Thread Safety**에 각별히 신경을 써야 합니다.
`Ecore Thread API`를 사용하였으며, 아래와 같이 뮤텍스를 사용하였습니다.
```c
//__preview_image_buffer_created_cb()
source = controller_mv_create_source(image_buffer->buffer,
image_buffer->buffer_size,
image_buffer->image_width,
image_buffer->image_height,
image_colorspace);
pthread_mutex_lock(&ad->mutex);
info = ad->latest_image_info;
ad->latest_image_info = NULL;
pthread_mutex_unlock(&ad->mutex);
free(info);
if (source)
controller_mv_push_source(source);
free(image_buffer);
pthread_mutex_lock(&ad->mutex);
if (!ad->image_writter_thread) {
ad->image_writter_thread = ecore_thread_run(__thread_write_image_file,
__thread_write_image_file_end_cb,
__thread_write_image_file_cancel_cb,
ad);
} else {
_E("Thread is running NOW");
}
pthread_mutex_unlock(&ad->mutex);
```
### 이미지 파일 저장 쓰레드 작업
다음은 쓰레드에서 이미지 파일을 저장하는 작업을 하는 부분의 코드입니다. `jpg` 인코딩하여 파일로 저장하고, 인코딩 데이터를 버퍼에 넣어두기도 합니다. 이미지 파일은 웹서버에서 웹 소켓으로 push로 전달 할 때 사용하고, 이미지 버퍼는 텔레그램으로 전송할 때 사용합니다.
```c
static void __thread_write_image_file(void *data, Ecore_Thread *th)
{
app_data *ad = (app_data *)data;
unsigned int width = 0;
unsigned int height = 0;
unsigned char *buffer = NULL;
unsigned char *encoded_buffer = NULL;
unsigned long long encoded_size = 0;
char *image_info = NULL;
int ret = 0;
pthread_mutex_lock(&ad->mutex);
width = ad->latest_image_width;
height = ad->latest_image_height;
buffer = ad->latest_image_buffer;
ad->latest_image_buffer = NULL;
if (ad->latest_image_info) {
image_info = ad->latest_image_info;
ad->latest_image_info = NULL;
} else {
image_info = strdup("00");
}
pthread_mutex_unlock(&ad->mutex);
ret = controller_image_save_image_file(ad->temp_image_filename, width, height, buffer,
&encoded_buffer, &encoded_size, image_info, strlen(image_info));
if (ret) {
_E("failed to save image file");
} else {
ret = rename(ad->temp_image_filename, ad->latest_image_filename);
if (ret != 0 )
_E("Rename fail");
}
pthread_mutex_lock(&ad->mutex);
free(ad->latest_encoded_image_buffer);
ad->latest_encoded_image_buffer = encoded_buffer;
ad->latest_encoded_image_buffer_size = encoded_size;
pthread_mutex_unlock(&ad->mutex);
free(image_info);
free(buffer);
}
```
## jpg 인코딩하기
Image Util로 파일을 저장하는 부분은 아래 가이드에 잘 설명이 되어 있습니다. 가이드에 나와있는대로 핸들러를 만들고 Encoding / Decoding 하는 코드를 `controller_image.c`에 구현하였으니 참고하시면 됩니다.
[Image Editing - Tizen Developer Site](https://developer.tizen.org/development/guides/native-application/multimedia/image-editing)
```c
int controller_image_save_image_file(const char *path,
unsigned int width, unsigned int height, const unsigned char *buffer,
unsigned char** encoded, unsigned long long* encoded_size, const char *comment, unsigned int comment_len)
{
int error_code = image_util_encode_set_resolution(encode_h, width, height);
//...생략
error_code = image_util_encode_set_colorspace(encode_h, IMAGE_COLORSPACE);
//...생략
error_code = image_util_encode_set_quality(encode_h, 90);
//...생략
error_code = image_util_encode_set_input_buffer(encode_h, buffer);
//...생략
error_code = image_util_encode_set_output_buffer(encode_h, encoded);
//...생략
error_code = image_util_encode_run(encode_h, encoded_size);
//...생략
error_code = exif_write_jpg_file_with_comment(path,
*encoded, (unsigned int)*encoded_size, width, height, comment, comment_len);
return error_code;
}
```
jpg로 인코딩 후에 그 데이터를 파일로 쓸 수 있으며, jpg 데이터를 텔레그램 또는 외부 서비스로 전송할 수 있습니다.
## Exif의 User Comment란
Exif 는 EXchangable Image File format 의 약자로서 이미지 파일 포맷입니다. 이미지 데이터이외에 메타 데이터를 저장하기에 용이하며 일반적으로 날짜와 시간 정보, 카메라 설정, 저작권 정보등을 포함 하고 있습니다.
>교환 이미지 파일 형식 (Exif; EXchangable Image File format)은 디지털 카메라에서 이용되는 이미지 파일 포맷이다. 이 데이터는 JPEG, TIFF 6.0과 RIFF, WAV 파일 포맷에서 이용되며 사진에 대한 정보를 포함하는 메타데이터를 추가한다. Exif는 JPEG 2000, PNG나 GIF 파일에서는 지원하지 않는다. - 위키피디아
본 프로젝트에서는 `APP1` 마커 중 `Exif IDF` 영역에 `User Comment` 태그에 모션 정보를 문자열로 저장하고 있습니다.
```c
int exif_write_jpg_file_with_comment(const char *output_file,
const unsigned char *jpg_data, unsigned int jpg_size,
unsigned int jpg_width, unsigned int jpg_height,
const char *comment, unsigned int comment_len);
```
## Exif에 부가 정보 넣기
Exif 에 부가 정보를 넣기 위해서 [libexif](https://libexif.github.io/)를 사용하여 `User Comment 태그`에 문자열을 저장하였습니다.
[libexif](https://libexif.github.io/)에 대해서 Tizen 개발자 사이트에 특별한 설명은 없지만, 해당 라이브러리의 공식 사이트의 정보를 확인하시고, 본 프로젝트에 사용된 코드를 참고하셔서 사용하시면 됩니다.
```c
int exif_write_jpg_file_with_comment(const char *output_file,
const unsigned char *jpg_data, unsigned int jpg_size,
unsigned int jpg_width, unsigned int jpg_height,
const char *comment, unsigned int comment_len)
{
int ret = 0;
unsigned char *exif_data = NULL;
unsigned int exif_size = 0;
if (!comment || (comment_len == 0)) {
_W("There is no comment");
return save_jpeg_file(output_file, jpg_data, jpg_size);
}
ret = create_exif_data(jpg_data, jpg_size, jpg_width, jpg_height,
comment, comment_len, &exif_data, &exif_size);
if (ret) {
_E("failed to create_exif_data(), save jpg data only");
return save_jpeg_file(output_file, jpg_data, jpg_size);
}
ret = save_jpeg_file_with_exif(
output_file, jpg_data, jpg_size, exif_data, exif_size);
free(exif_data);
return ret;
}
```
# 모니터링을 위한 IoTjs Web 서버
앞에서 카메라를 통한 이미지를 분석한 결과를 Exif에 넣어 jpg 파일을 생성하였습니다. 그 파일을 실시간으로 확인하기 위해서 Web Server를 만들었습니다.

Web Server Application은 IoT.js를 이용해서 web server를 구현한 것입니다. 브라우저 상에서 본 프로젝트의 내용을 확인 할 수 있는 static 파일들(html, css, js)을 배포하고, `Web Socket`을 통해서 실시간으로 현재 캡쳐된 이미지 파일을 Push 하는 역할을 합니다.
**IoTjs는 Tizen IoT에서 웹 기술을 쉽게 사용할 수 있는 강력한 기술입니다.** IoTjs를 이용해서 타이젠에서 어렵지 않게 간단한 웹 서버를 만드는 방법을 소개하려 합니다. **웹서버 이외에도 다양한 웹 기술과 라이브러리등을 쉽게 사용할 수 있으니 프로젝트의 목적에 맞게 적절히 사용하시면 큰 도움이 될 수 있을것 같습니다.**
[IoT.js 공식 홈페이지](https://iotjs.net/)
>IoT.js aims to provide inter-operable service platform in the world of IoT, based on web technology. The target of IoT.js is to run in resource constrained devices such as only few kilobytes of RAM available device. Thus it will supports very wide range of "Things".
## IoTjs 준비하기
`IoTjs`는 `JerryScript`라는 Javascript 엔진을 사용하여 만들어진 IoT Framework 입니다. 쉽게 비유하자면 IoT를 위한 `nodejs` 같은 것이라고 생각하셔도 좋을것 같습니다.
이미 Tizen 5.0 에 포함되어 있으며 별다른 준비가 필요없지만, 5.0에 포함된 버전은 Web socket 기능이 꺼진채로 빌드된 버전이라 Web socket을 사용하기 위해서는 별도의 빌드가 필요합니다. 그래서 아래 `/RPMS/IoTjs` 폴더에 빌드해서 넣어 두었습니다.
아래 커맨드로 타겟 디바이스에 설치해 주시면 됩니다.
```bash
$sdb root on
$sdb push ./RPMS/IoTjs/iotjs-1.0.0-99.armv7l.rpm /tmp
$sdb shell 'mount -o remount,rw /'
$sdb shell 'rpm -ivh --force /tmp/iotjs-1.0.0-99.armv7l.rpm'
```
## IoTjs Application 시작하기
일반적으로 `nodejs` 프로그램도 `pm2`와 같은 프로세스 메니저로 프로세스를 관리하곤 하는데 **Tizen에서도 IoTjs를 Tizen App으로 만들면 기존의 Tizen Application Life-cycle 을 그대로 사용할 수 있습니다.** 즉, Tizen Application 처럼 실행하고, 설치, 삭제 할 수 있습니다.
본 프로젝트의 `dashboard` 폴더를 타이젠 스튜디오에 import 후 빌드하여 설치하면 됩니다.
```c
int main(int argc, char* argv[])
{
char ad[50] = {0,};
service_app_lifecycle_callback_s event_callback;
app_event_handler_h handlers[5] = {NULL, };
event_callback.create = service_app_create;
event_callback.terminate = service_app_terminate;
event_callback.app_control = service_app_control;
service_app_add_event_handler(&handlers[APP_EVENT_LOW_BATTERY], APP_EVENT_LOW_BATTERY, service_app_low_battery, &ad);
service_app_add_event_handler(&handlers[APP_EVENT_LOW_MEMORY], APP_EVENT_LOW_MEMORY, service_app_low_memory, &ad);
service_app_add_event_handler(&handlers[APP_EVENT_LANGUAGE_CHANGED], APP_EVENT_LANGUAGE_CHANGED, service_app_lang_changed, &ad);
service_app_add_event_handler(&handlers[APP_EVENT_REGION_FORMAT_CHANGED], APP_EVENT_REGION_FORMAT_CHANGED, service_app_region_changed, &ad);
return iotjs_service_app_start(argc, argv, "server.js", &event_callback, ad);
}
```
코드를 보시면 일반적인 `service application`과 거의 동일 하다는 것을 알 수 있습니다. **`iotjs_service_app_start`으로 `Javascript` 파일 `server.js`를 실행시키는 부분이 유일한 차이점 이며, 그 부분이 IoTjs Tizen Application을 만드는 핵심 부분입니다.`**
## IoTjs 웹서버 만들기
IoTjs를 사용하면 손쉽게 웹서버를 만들 수 있습니다. 웹서버는 여러가지 기능을 수행할 수 있지만, 본 프로젝트에서는 아주 간단한 역할만을 수행하도록 하였습니다.
`res/server.js` 의 코드를 확인해 보시면 100줄 안팍의 코드로 static file들을 배포하고, Web socket을 사용해서 마지막 이미지 프레임을 push해주는 것을 구현하였습니다.
```javascript
http.createServer(function(req, res) {
req.on('end', function() {
var path = extractPath(req.url);
console.log(req.url)
// var last = path[path.length - 1];
if (path[0] === undefined) {
res.writeHead(200);
res.end(fs.readFileSync(SERVER_ROOT_FOLDER_PATH + 'public/index.html'));
} else if (path[0] == 'test') {
res.writeHead(200);
res.end(fs.readFileSync(SERVER_ROOT_FOLDER_PATH + 'public/test.html'));
} else if (req.url == '/js/app.js') {
res.writeHead(200);
res.end(fs.readFileSync(SERVER_ROOT_FOLDER_PATH + 'public/js/app.js'));
} else if (req.url == '/css/style.css') {
res.writeHead(200);
res.end(fs.readFileSync(SERVER_ROOT_FOLDER_PATH + 'public/css/style.css'));
} else {
// res.setHeader('Location', 'http://download.tizen.online/smart-surveillance' + req.url);
// res.writeHead(302);
res.end();
}
});
}).listen(9090);
```
위의 코드는 static file들을 배포하는 코드 입니다. image 리소스 같은 것은 download.tizen.online의 별도 웹 스토리지를 사용하게 할 수도 있습니다.
### Web socket 사용하기
앞에서도 설명한 것과 같이 IoTjs에서 Web Socket을 사용하기 위해서는 별도로 빌드가된 IoTjs 패키지가 설치되어 있어야 합니다.
아래 코드는 Web socket으로 이미지를 Push하는 코드입니다. 정말 너무 간단해서 설명할 것이 별로 없습니다. Smart Surveillance Camera Application 의 `shared/res` 에 저장된 `latest.jpg`파일을 읽어서 웹소켓으로 Push 하고 있습니다.
```js
var websocket = require('websocket');
var options = {
port: 8888
}
var server = new websocket.Server(options, Listener);
function Listener(ws) {
console.log('Client connected: handshake done!');
ws.ack = true;
ws.on('message', function (msg) {
console.log('Message received: %s', msg.toString());
// ws.send(msg.toString(), {mask: true, binary: false}); //echo
// ws.send('Received: ' + msg.toString()); //echo
// server.close();
ws.ack = true;
});
ws.on('ping', function (msg) {
console.log('Ping received: %s', msg.toString());
});
ws.on('error', function (msg) {
console.log('Error: %s', msg.toString());
});
var i = 0;
var prev = 0;
var timeout = setInterval(function() {
if (!ws.ack)
return false;
var now = Date.now();
var data;
try {
data = fs.readFileSync(LATEST_FRAME_FILE_PATH);
} catch (err) {
console.log(err);
data = fs.readFileSync(SERVER_ROOT_FOLDER_PATH + 'default.gif');
}
ws.send(data, {mask: false, binary: true});
console.log(`Sending frame(${i++}), interval(${now - prev} ms)`);
// server.broadcast(data, {mask: false, binary: true});
// server.broadcast(`HELLO TO ALL FROM IoT.js!!! (${i++}, ${now - prev})`);
prev = now;
ws.ack = false;
}, 1000 / 15.0);
```
## 모니터링을 위한 Web application
앞에서 IoTjs를 사용하여 Web Server를 구성하였습니다. 이제 브라우저를 사용하여 웹 서버에 접속하였을 때 보여지는 화면을 구성하는 Web Application에 대해서 설명드리겠습니다.
Web Application은 일반적인 Web Application과 동일합니다. `respublic` 폴더 아래에 static 파일들이 있습니다.
아래 코드는 웹 소켓으로 jpg 이미지 데이터를 수신하는 코드입니다. Exif 데이터를 디코딩하기 위해서 [exif.js 라이브러리](https://github.com/exif-js/exif-js)를 사용하였습니다.
```javascript
function runWebSocket() {
var wsUri = "ws://" + window.location.hostname + ":8888/";
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}
function onOpen(evt)
{
console.log("CONNECTED");
doSend("HELLO FROM BROWSER via WebSocket!!!!!!!!!!!!");
}
function onClose(evt)
{
console.log("DISCONNECTED");
}
function onMessage(evt)
{
var urlCreator = window.URL || window.webkitURL;
var imageUrl = urlCreator.createObjectURL(evt.data);
document.querySelector("#camera-view").src = imageUrl;
var arrayBuffer;
var fileReader = new FileReader();
fileReader.onload = function(event) {
arrayBuffer = event.target.result;
var exif = EXIF.readFromBinaryFile(arrayBuffer);
var exifInfoString = asciiToStr(exif.UserComment, 8);
var type = 'blur';
if (getResultType(exifInfoString) != 0) {
type = 'active';
}
var pointArray = getPointArrayFromString(exifInfoString.slice(4));
canvas.clearPoints();
canvas.drawPoints(pointArray, type);
};
fileReader.readAsArrayBuffer(evt.data);
doSend("ack", true);
}
```
# 텔레그램 봇으로 알림 메세지 전달하기
우리는 하루에도 수십 수백건의 Push 알림을 받습니다. Push 알림은 수많은 사람의 시간을 절약해주고 사회 전반의 여러 시스템의 효율을 극대화시키는데 큰 공을 세웠습니다.
**쉽고 간단하게 Push 알림을 사용할 수 있는 방법으로 텔레그램 봇을 사용하였습니다.**

텔레그램으로 메시지를 보내고, 사진을 보내지만 **결국은 `http` 프로토콜을 사용하여 `REST API`를 사용하는 것입니다. 프로젝트를 다른 서비스들과 연동하여 확장성을 높일 수 있는 방법에 대한 예시가 되길 바라며** 기존 프로젝트에서 텔레그램 부분을 추가하였습니다.
## 텔레그램 봇 만들기
외부와 연결하는 방법, Push 서비스는 여러가지가 있지만 텔레그램을 선택한 이유는 매우 쉽고 간단하기 때문입니다. 봇을 만들고 간단하게 연동하는 방법을 설명드리겠습니다.
텔레그램을 설치합니다. `BotFather`를 검색하여 대화를 시작합니다. `/newbot`이라는 메세지를 보내면 생성할 봇의 이름과 봇의 사용자 이름을 물어옵니다. 만들고 싶으신 대로 응답을 하면 토큰을 발급해 줍니다.
이렇게 만들어진 토큰을 `controller_telegram.c`에 넣어 주면됩니다. 정말 쉽죠? :)
```c
#define TELEGRAM_BOT_INFO "{YOUR_BOT_TOKEN}"
```
### 텔레그램 봇 API 소개
본 프로젝트에서 사용한 텔레그램 봇 API는 3개에 대해 간단하게 설명드리겠습니다. 더 자세한 내용이나 추가 기능은 아래 레퍼런스를 확인해 보시면 정리가 매우 잘 되어 있습니다.
[Telegram Bot API](https://core.telegram.org/bots/api)
>The Bot API is an HTTP-based interface created for developers keen on building bots for Telegram.
To learn how to create and set up a bot, please consult our Introduction to Bots and Bot FAQ.
```javascript
https://api.telegram.org/bot{YOUR_BOT_TOKEN}/getUpdates
```
`getUpdate`는 봇이 수신한 메세지를 확인하는 API입니다. 대화방 아이디를 알아내기 위해서 사용합니다.
```javascript
https://api.telegram.org/bot{YOUR_BOT_TOKEN}/sendMessage?text={TEXT_MSG}&chat_id={CHATROOM_ID}
```
`sendMessage`는 봇이 text 메세지를 보내는 방법입니다. `getUpdate`로 알아낸 대화방 아이디를 사용합니다.
```javascript
https://api.telegram.org/bot{YOUR_BOT_TOKEN}/sendPhoto?chat_id={CHATROOM_ID}
```
`sendPhoto`는 봇이 이미지를 보내는 방법입니다. 텔레그램에 파일을 보내는 방법은 3가지가 있습니다.
1. 기존 텔레그램에 공유된 파일의 `file_id` 를 사용하는 방법
1. Web에 올라가 있는 URL을 이용한 방법
1. Content Type `multipart/form-data`로 payload에 이미지 데이터를 직접 업로드
## 텔레그램 텍스트 메시지 보내기
텍스트 메시지는 url 에 토큰과 메세지를 넣어서 `GET` 메소드로도 전송이 가능합니다. 즉, 브라우저를 이용해서도 간단하게 테스트를 해볼 수 있습니다.
브라우저를 사용해 PC에서 먼저 테스트를 해보고 아래 대화방 아이디를 입력하여 실행하며 됩니다.
```c
#define TELEGRAM_CHATROOM_INFO "{CHATROOM_ID}"
```
아래는 텍스트 메세지를 보내는 함수입니다. `CURL` 라이브러리를 이용해서 간단하게 구성했습니다.
```c
int controller_telegram_send_message(const char* msg)
{
//...생략
char* url_with_msg = g_strdup_printf("%s%s%s%s&chat_id=%s",
TELEGRAM_API_HOST_URL,
TELEGRAM_BOT_INFO,
TELEGRAM_BOT_SEND_MSG_URL,
msg,
TELEGRAM_CHATROOM_INFO);
_D("Send TEXT Url: [%s]", url_with_msg);
curl_easy_setopt(curl, CURLOPT_URL, url_with_msg);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _response_callback);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, REQ_CON_TIMEOUT);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, REQ_TIMEOUT);
response = curl_easy_perform(curl);
if (response != CURLE_OK) {
_E("curl_easy_perform() failed: %s",
curl_easy_strerror(response));
/* What should we do here, if response is kind of errors? */
ret = -1;
}
g_free(url_with_msg);
curl_easy_cleanup(curl);
return ret;
}
```
[CURL](https://curl.haxx.se)은 데이터를 전송하기 위해 가장 많이 사용되는 클라이언트 라이브러리 중 하나입니다.
> cURL(/kɝl/ 또는 /kə:l/[3])은 다양한 통신 프로토콜을 이용하여 데이터를 전송하기 위한 라이브러리와 명령 줄 도구를 제공하는 컴퓨터 소프트웨어 프로젝트이다. 이 cURL 프로젝트는 libcurl와 cURL이라는 2개의 제품을 만든다. 1997년에 처음 출시되었다. 이 이름은 "client URL"을 대표한다. - 위키피디아
## 텔레그램 사진 메시지 보내기
텔레그램에서 사진 메시지를 보내는 방법은 텍스트 메세지와 비슷하지만 이미지 데이터를 보내야 하기 때문에 `POST` 메소드를 사용합니다.
`CURL` 을 사용하여 이미지 데이터를 전송할 때 파일에서 읽어와서 전송할 수도 있지만, encoded 된 image 데이터를 저장해 뒀기 때문에 그 버퍼를 바로 사용하였습니다.
```c
int controller_telegram_send_image(const unsigned char* image_buffer, unsigned int buffer_size)
{
//...생략
char* url_with_msg = g_strdup_printf("%s%s%s?chat_id=%s",
TELEGRAM_API_HOST_URL,
TELEGRAM_BOT_INFO,
TELEGRAM_BOT_SEND_PHOTO_URL,
TELEGRAM_CHATROOM_INFO);
_D("Send PHOTO Url: [%s]", url_with_msg);
curl_formadd(&formpost, &lastptr,
CURLFORM_COPYNAME, "content-type:",
CURLFORM_COPYCONTENTS, "multipart/form-data",
CURLFORM_END);
curl_formadd(&formpost, &lastptr,
CURLFORM_COPYNAME, "photo",
CURLFORM_BUFFER, "motion.jpg",
CURLFORM_BUFFERPTR, image_buffer,
CURLFORM_BUFFERLENGTH, buffer_size,
CURLFORM_END);
curl_easy_setopt(curl, CURLOPT_URL, url_with_msg);
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _response_callback);
/* if CURLOPT_VERBOSE is enabled, __curl_debug() function will be called */
// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, __curl_debug);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, REQ_CON_TIMEOUT);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, REQ_TIMEOUT);
response = curl_easy_perform(curl);
if (response != CURLE_OK) {
_E("curl_easy_perform() failed: %s",
curl_easy_strerror(response));
ret = -1;
}
curl_easy_cleanup(curl);
curl_formfree(formpost);
g_free(url_with_msg);
return ret;
}
```
## 별도 쓰레드에서 네트워크 사용하기
앞서 jpg 파일을 저장할 때와 카메라 프리뷰 이미지를 처리할 때 쓰레드를 하나씩 사용했습니다. 마찬가지로 **네트워크를 사용할 때도 쓰레드를 사용해야 합니다.**
아래는 쓰레드를 사용하여 텔레그램 메세지 전송 작업을 처리하는 코드입니다. `TELEGRAM_EVENT_INTERVAL_MS`는 텔레그램 메세지를 전송하는 최소 간격을 설정하기 위해 사용하였습니다.
```c
static void __send_telegram_message(const char* msg, app_data *ad)
{
if (!msg)
return;
static long long int last_event_time = 0;;
long long int now = __get_monotonic_ms();
if (now < last_event_time + TELEGRAM_EVENT_INTERVAL_MS) {
return;
}
last_event_time = now;
if (ad->telegram_message)
free(ad->telegram_message);
ad->telegram_message = strdup(msg);
free(ad->telegram_image_buffer);
pthread_mutex_lock(&ad->mutex);
ad->telegram_image_buffer = ad->latest_encoded_image_buffer;
ad->latest_encoded_image_buffer = NULL;
ad->telegram_image_buffer_size = ad->latest_encoded_image_buffer_size;
pthread_mutex_unlock(&ad->mutex);
if (!ad->telegram_thread) {
ad->telegram_thread = ecore_thread_run(__thread_telegram_task,
__thread_telegram_task_end_cb,
__thread_telegram_task_end_cb,
ad);
} else {
_E("Telegram Thread is running NOW");
}
}
```
Notice
Are you sure to delete this post?
Television
1
0
|
Last modified on September 27, 2019
Craft info. | |
Maker |
![]() |
Status | In Progress |
Period | 2019-09-08 ~ 2019-09-08 |
About This Craft | |
Television Project - Smart Surveillance Camera with Telegram | |
Making Note