接触確認アプリ FolkBears では、接触データを Aware というサーバーに送信しています。Aware framework https://awareframework.com/ は、モバイルから加速度データとか WiFi 取得のデータとかをうまく収集できる研究用アプリ+サーバーで、結構便利…なんですが、FolkBears の BLE アドバタイズと利用した接触データの収集とはちょっと相性が悪くてですね(これは数年後に気付いたわけですが)、FolkBears 内部では切り離しを進行中です。
が、データ収集部分は Aware を使わないとしても、データ収集をするためのサーバーのほうは aware サーバーでもよいか、ということでそのままになっています。このサーバーコードが aware-micro https://github.com/denzilferreira/aware-micro な訳ですが。さて、これ以前は PHP で書かれていたような気がするのですが、現在は Kotlin で書かれています。Kotlin はまあいいんですが、内部的に Eclipse Vert.x https://vertx.io/ を使っていてですね、ちょっと再構築にコツが入りそうなのです。
というか、1年程前に自前でサーバーを構築しようして失敗したものだから、面倒くさくなって Azure Functions で仮実装して実験的に動かしていました。ごく一部分だけ使うわけだし、FolkBears からのデータアップロードだけ部分なので、C# で書いても大して手間ではなかったのですが。が、内部で SQL Server を使って、暫くほっといていたら月2万円の請求になってしまい(無料枠が1.2万円ほどだたので、実際の支払は8000円ほどです)、これは放置するとアカンなと思った次第です。
本格的にデータ収集するとなるとギガバイト単位の DB になってしまうので考え直さないといけないのですが、実験データ程度ならば aws の EC2 + t3.micro でもいいはずです。まあ、安く済ませるならば、さくらの VPS を借りて PHP で実装したほうがいいのですが、それば別の機会にでも。
以下、試行錯誤も含めて作業ログ的に書き進めていきます。
docker-compose.yml の作成
aware-micro https://github.com/denzilferreira/aware-micro に Dockerfile の例があるので Ubuntu + Docker で構築することを考えます。
FROM openjdk:11
# Set the location of the verticles
ENV VERTICLE_HOME /usr/verticles
# Set the name of the verticle to deploy
ENV VERTICLE_AWARE_JAR micro-1.0.0-SNAPSHOT-fat.jar
ENV VERTICLE_AWARE_CONFIG aware-config.json
EXPOSE 8080
# Set vertx option
ENV VERTX_OPTIONS ""
# Copy your verticle and configuration to the container
COPY $VERTICLE_AWARE_JAR $VERTICLE_HOME/
COPY $VERTICLE_AWARE_CONFIG $VERTICLE_HOME/
WORKDIR $VERTICLE_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $VERTICLE_AWARE_JAR"]
git から aware-micro のコードを持ってきてビルドするのではなくて、あらかじめ micro-1.0.0-SNAPSHOT-fat.jar を作成しておく方式です。設定ファイルとして aware-config.json も必要そうです。以前は、これをどうやってつくるのか?が分からなかったのですが、aware-micro https://github.com/denzilferreira/aware-micro の readme に書いてありました。
こっちのほうは、実際にサーバーを動かす方
cd aware-micro
./gradlew clean build run
こっちのほうが、*.jar ファイルを作る方
cd aware-micro
./gradlew clean build shadowJar

build/lib/micro-1.0.0.SNAPSHOT-fat.jar というファイルができあがります。build.gradle の kotlinOptions.jvmTarget を最新の 17 にしていますが通ります。dependencies にあるライブラリのバージョンが古いかもしれませんが、ひとまずこのままにしておきます。
Copilot を使って docker-compose.yml を作って貰います。
version: "3.9"
services:
mysql:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: aware
MYSQL_USER: aware
MYSQL_PASSWORD: awarepass
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
interval: 10s
timeout: 5s
retries: 5
aware-micro:
build:
context: dev-local
dockerfile: Dockerfile
image: aware-micro:dev
depends_on:
mysql:
condition: service_healthy
environment:
VERTX_OPTIONS: ""
ports:
- "8080:8080"
volumes:
- ./dev-local/aware-config.json:/usr/verticles/aware-config.json:ro
restart: unless-stopped
volumes:
mysql-data:
volumes 部分を変更して、aware-config.json と micro-1.0.0-SNAPSHOT-fat.jar をコピーできるようにします。このファイルは aware-micro プロジェクトの方からコピーします。
volumes:
- ./aware-config.json:/usr/verticles/aware-config.json:ro
- ./micro-1.0.0-SNAPSHOT-fat.jar:/usr/verticles/micro-1.0.0-SNAPSHOT-fat.jar:ro
aware-config.json の server の設定を変えておきます。データベース名は docker-compose.yml のほうに合わせました。database_host は docker-composer にある “mysql” を使う筈。
{
"server" : {
"database_engine" : "mysql",
"database_host" : "mysql",
"database_name" : "aware",
"database_user" : "aware",
"database_pwd" : "awarepass",
"database_port" : 3306,
"server_host" : "http://localhost",
"server_port" : 8080,
"websocket_port" : 8081,
"path_fullchain_pem" : "",
"path_key_pem" : ""
},
"study" : {
"study_key" : "4lph4num3ric",
"study_number" : 1,
"study_name" : "AWARE Micro demo study",
"study_active" : true,
"study_start" : 1770433225185,
"study_description" : "This is a demo study to test AWARE Micro",
"researcher_first" : "First Name",
"researcher_last" : "Last Name",
"researcher_contact" : "your@email.com"
},
"sensors" : [ {
"sensor" : "accelerometer",
"title" : "Accelerometer",
micro-1.0.0-SNAPSHOT-fat.jar の作成
./gradlew clean build run のほうはサーバー立ち上げなので 8080 番でポート待ちです。

*.jar ファイルを作るのは ./gradlew clean build shadowJar のほう

aware-config.json ファイルは ./gradlew clean build run あるいは ./gradlew run で自動生成されます。
Docker コンテナを作成&実行
ひとまず Windows 上で docker compose up -d を試します。

Javaのランタイムバージョンでこけるので、Dockerfile の先頭を変えておきます。
FROM eclipse-temurin:17-jre
micro-1.0.0-SNAPSHOT-fat.jar をビルドしたときのバージョンとあわせておきます。

無事 Docker コンテナが立ち上がれば ok です。私の環境の場合、ホストPCに MySQL が入っているので、”3307:3306″ にしておきます。
MySQL Workbench で接続できるところまで確認します。
ブラウザから http://localhost:8080/ のように接続すると、こんな形に出れば aware サーバーが動いています。

テーブルを作成する
データベースにテーブルを作成しておきます。もともと aware-micro にはテーブルを作成する機能があるのですが、これがコメントアウトされています。まあ、URL を通してばかすかテーブルを作られてしまっても困るので、ガードしてあります。
その中で createTable メソッドがあるので、次のようにテーブル作ります。
CREATE TABLE IF NOT EXISTS `$table`
(
`_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`timestamp` DOUBLE NOT NULL,
`device_id` VARCHAR(128) NOT NULL,
`data` JSON NOT NULL,
INDEX `timestamp_device` (`timestamp`, `device_id`)
)
`$table` のところは実際に作成するテーブル名をいれます。クライアントから aware-micro に送られたデータは、data カラムに入ります。data カラムは JSON 形式なのでとにかく JSON にしておくと何でも突っ込めます。逆に言えば JSON 形式しか突っ込めません。
クライアント(FolkBears)からデータを送信する
aware-micro サーバーが出来上がったところで、データを送信してみます。aware-micro に送信するときにフォーマットがちょっとややこしくて、
- POST メソッドを使う
- content-type を x-www-form-urlencoded 形式で送る
- device_id パラメータが必須
- JSON 形式は必ず配列にして data パラメータで送る(複数データが前提になっている!)
という形式になっています。
curl コマンドで送信するときは以下の形式です。
curl -X POST "http://localhost:8080/index.php/1/4lph4num3ric/sample/insert" `
-H "Content-Type: application/x-www-form-urlencoded" `
-d "device_id=xxx" `
-d 'data=[{"timestamp": 1, "name": "masdua"}]'
URL がまたややこしくて、aware-config.json に記述した、study_number と study_key を使います。index.php は昔 PHP で作成したときの名残りっぽいです。たぶん、もともとは加速度センサーなどの情報を一気に入れるようになったものを、micro にして汎用化したんじゃないかなぁと。
呼び出す URL の形式はこんな感じ
/index.php/:studyNumber/:studyKey/:table/:operation
- index.php: 固定
- :studyNumber: aware-config.json の study_number
- :studyKey: aware-config.json の study_key
- :table: 操作するテーブル名
- :operation: 操作方法(insert, update など)
で、無事データが insert されると、こんな風になります。

ひとまず、aware-micro の Docker コンテナに送信できたので、一旦終了。
FolkBears Android 版からの送信
この方式だと微妙に面倒なので、retrofit2 ライブラリを使っています。
package jp.mamori_i.app.webapi
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface AwareService {
@FormUrlEncoded
@POST("trace_data/insert")
fun uploadData(
@Field("device_id") device_id: String,
@Field("data") data: String
): Call<Void>
@FormUrlEncoded
@POST("trace_data/query")
fun queryData(
@Field("device_id") device_id: String,
@Field("start") start: Long,
@Field("end") end: Long
): Call<String>
@FormUrlEncoded
@POST("temp_user_id/insert")
fun uploadTempUserId(
@Field("device_id") device_id: String,
@Field("data") data: String
): Call<Void>
@FormUrlEncoded
@POST("temp_user_id/query")
fun queryTempUserId(
@Field("device_id") device_id: String,
@Field("start") start: Long,
@Field("end") end: Long
): Call<String>
@FormUrlEncoded
@POST("deep_contact_user/insert")
fun uploadDeepContactUser(
@Field("device_id") device_id: String,
@Field("data") data: String
): Call<Void>
@FormUrlEncoded
@POST("deep_contact_user/query")
fun queryDeepContactUser(
@Field("device_id") device_id: String,
@Field("start") start: Long,
@Field("end") end: Long
): Call<String>
}
object RetrofitClient {
private const val BASE_URL = "https://aware....jp/index.php/1/folkbears/"
val awareService: AwareService by lazy {
retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(retrofit2.converter.scalars.ScalarsConverterFactory.create())
.build()
.create(AwareService::class.java)
}
}
実際の呼び出しは TraceDataUpload.upload で。
package jp.mamori_i.app.webapi
import com.google.gson.Gson
import jp.mamori_i.app.data.TraceDataEntity
import jp.mamori_i.app.webapi.RetrofitClient
class TraceDataUpload
{
/**
* Aware に POST 送信する body を作成する
*/
fun fromTraceDataList(items: List<TraceDataEntity> ) : String {
if ( items.isEmpty() ) {
// データがない場合は空配列を返す
return "[]"
}
/**
* 送信用の TraceData のデータクラス
*/
data class TraceData(
val temp_id: String,
val timezone: Int,
val timestamp: Long,
val tx_power: Int,
val jsonVersion: Int,
val os: String,
val rssi: Int,
val label: String
){}
var lst = mutableListOf<TraceData>()
items.map {
lst.add( TraceData(
temp_id = it.tempId,
timezone = 9,
timestamp = it.timestamp,
tx_power = 0,
jsonVersion = 0,
os = "android",
rssi = it.rssi,
label = ""
))
}
return Gson().toJson(lst)
}
fun upload( deviceId: String, data: String) : Boolean {
val response = RetrofitClient.awareService
.uploadData(device_id = deviceId, data = data )
.execute()
return response.isSuccessful
}
