編集履歴一覧に戻る
tsuji1のアイコン画像

tsuji1 が 2024年01月31日02時44分34秒 に編集

初版

タイトルの変更

+

捨てにくいゴミ箱?ゴミ問題を解決するSpresenseを使ったスマートゴミ箱-

タグの変更

+

SPRESENSE

+

ELTRES

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

ライセンスの変更

+

(GPL-3.0+) GNU General Public License, version 3

本文の変更

+

![キャプションを入力できます](https://camo.elchika.com/7527baf52626d88076bbaabc997f7138baec7145/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39653363386531352d633733362d346433332d616461622d3539663863393665626431342f33363162373739622d663564382d346437382d396463302d353738363036336130396539/) # 概要 今回作成したスマートゴミ箱は**ごみ問題**を解決するために作成した主に市などの行政を対象としたスマートゴミ箱です。 設置されたスマートゴミ箱はSpresenseが指定したQRコードを読み取ることで開くことができます。そのQRコードはウェブサイトを通じて確認することができますが、必要なQRコードを確認するためには動画を見なければなりません。 様々なごみ問題がありますが、**手間をかけさせること**で街中のごみ箱の使用を減らすという発想を基にSpresenseで捨てにくいスマートゴミ箱を作成しました。 Eltres通信は**相互通信はできない**のですがQRコードを利用することにより、一方通信のセキュリティ的な利点を潰さずにSpresenseでゴミ箱を開くタイミングを利用者に決定させることが可能となりました。 # 背景 まずSpresenseの非常に*省電力*なところは野外などに置いておくときに非常に有利な点を持つなと思い、市中のごみ箱を想定してためになるものを作成しようと考えました。 初めは捨てやすいごみ箱を作ろうとしていました。しかしながら、捨てることでポイントを付与するというアイデアはすでにもう実現されていましたし、何より捨てられるゴミの量が増えたとしても、行政団体は何も*得*がありません。それならば、街中捨てにくいごみ箱になれば人々は捨てるのが面倒となって*そもそも出すゴミの量が減る*のではないかと考えました。 # 利用について 1. まずごみ箱利用者はごみ箱に貼られているQRコードを読んで[Webサイト](http://tsuji1.mydns.jp/)を開きます。 2. 開いたら動画が出てくるのでそれを視聴します。 3. 動画視聴後にボタンが出現するのでそれを押して、QRコードを表示させます。 ここまでの流れを動画として表示します。 @[youtube](https://www.youtube.com/watch?v=0Jf4JUuq2fk) 4. 出てきたQRコードをSpresenseのカメラにかざします。 5. ゴミ箱が開きます。 ゴミ箱が開く様子を下記の動画で確認してください。 @[youtube](https://www.youtube.com/watch?v=GvA5AGiRJDk) # システムについて ## システム概要 ### 利用者の手引き ![キャプションを入力できます](https://camo.elchika.com/89fafd70c2054146e293fc1f5de3e21dd858b307/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39653363386531352d633733362d346433332d616461622d3539663863393665626431342f33623638343436392d333331352d343266322d383064612d363836353433656265366261/) 利用者は上記画像のように 1. QRコードを動画を視聴して取得して 2. 取得したQRコードをSpresenseカメラにかざして 3. QRコードがSpresenseが発行したQRコードと一致すればスマートゴミ箱が開く という流れになっています。 ### QRコードなどのデータの送受信の流れ ![キャプションを入力できます](https://camo.elchika.com/3b1d7cbdbe4ad8f3e521a57b25fe2e7304d405e6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39653363386531352d633733362d346433332d616461622d3539663863393665626431342f30386166303831302d373934652d343066632d396232352d393633613530386233643761/) システムは上記画像のように 1. Spresenseがランダムな16進数のキー作成 2. 1分おきに、生成されたキーをEltres通信を使って送信 3. APIサーバーがユーザーがQRコードを取得する前に生成されたキーをAPIで取得 4. APIサーバーが受け取ったキーをWebサーバーでQRコードに変換してから表示 5. 表示されたQRコードを使って利用者がゴミ箱を開けたらキーの再生成 という流れになっています。 前述で説明されたEltres通信が一方向通信であるという問題は、APIサーバーを用意して**非同期**で、生成されたキーを取得することで解決しています。 ## 製作手順 ## 利用物 - Spresense - Spresenseメインボード - Spresense拡張ボード - ELTRESアドオンIoT開発キット一式 - Spresense HDRカメラボード - サーボモータSG90 - 適当な正方形の木の板 - ゴミ箱を作成するのに使いました。 - Webサイトが動くサーバー ## ゴミ箱本体の作成 [https://www.amazon.co.jp/gp/product/B0BW321ZWK/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&psc=1](Amazon)で購入したDIY用の木材を6つ使って作成しました。 5つはグルーガンで接着させて、サーボモータは下記の写真のように張り付けた。 サーボモータ本体を蓋の方につけると安定しました。 ![キャプションを入力できます](https://camo.elchika.com/92dca00e94b19b4dbedaae0fee7768a4491488e5/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39653363386531352d633733362d346433332d616461622d3539663863393665626431342f65663464353430622d396462302d343532302d393431362d363236333236363631633330/) ### ソースコード Spresenseに書き込むコードを紹介する。「eltres_sample_gps.ino - GPS情報を送信するサンプルプログラム」をベースにしてます。 CamCB関数はQRコードを読み取る際に用いるコードで[こちらのライブラリ](https://github.com/YoshinoTaro/QR_decode_for_Arduino)を利用させてもらいました。 ```C #include <EltresAddonBoard.h> #include <Camera.h> #include <Servo.h> #include "quirc.h" String qr_code_str; String ans_str; struct quirc *qr; struct quirc_code code; struct quirc_data data; uint8_t *image; int w, h; bool is_set_payload_qr = false; bool is_camera_streaming = false; static Servo s_servo_1; String new_ans_str; // PIN定義:LED(プログラム状態) #define LED_RUN PIN_LED0 // PIN定義:LED(GNSS電波状態) #define LED_GNSS PIN_LED1 // PIN定義:LED(ELTRES状態) #define LED_SND PIN_LED2 // PIN定義:LED(エラー状態) #define LED_ERR PIN_LED3 // プログラム内部状態:初期状態 #define PROGRAM_STS_INIT (0) // プログラム内部状態:起動中 #define PROGRAM_STS_RUNNING (1) // プログラム内部状態:終了 #define PROGRAM_STS_STOPPED (3) // 撮影サイズ #define PICTURE_WIDTH (CAM_IMGSIZE_QVGA_H) #define PICTURE_HEIGTH (CAM_IMGSIZE_QVGA_V) int nextSendID = 0; // プログラム内部状態 int program_sts = PROGRAM_STS_INIT; // GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ bool gnss_recevie_timeout = false; // 点滅処理で最後に変更した時間 uint64_t last_change_blink_time = 0; // イベント通知での送信直前通知(5秒前)受信フラグ bool event_send_ready = false; // ペイロードデータ格納場所 uint8_t payload_for_gnss[16]; uint8_t payload_for_qr[16]; // 最新のGGA情報 eltres_board_gga_info last_gga_info; bool change_payload=false; /**  * @brief イベント通知受信コールバック  * @param event イベント種別  */ void eltres_event_cb(eltres_board_event event) {   switch (event)   {   case ELTRES_BOARD_EVT_GNSS_TMOUT:     // GNSS電波受信タイムアウト     Serial.println("gnss wait timeout error.");     gnss_recevie_timeout = true;     break;   case ELTRES_BOARD_EVT_IDLE:     // アイドル状態     Serial.println("waiting sending timings.");     digitalWrite(LED_SND, LOW);     break;   case ELTRES_BOARD_EVT_SEND_READY:     // 送信直前通知(5秒前)     Serial.println("Shortly before sending, so setup payload if need.");     event_send_ready = true;     break;   case ELTRES_BOARD_EVT_SENDING:     // 送信開始     Serial.println("start sending.");     digitalWrite(LED_SND, HIGH);     break;   case ELTRES_BOARD_EVT_GNSS_UNRECEIVE:     // GNSS電波未受信     Serial.println("gnss wave has not been received.");     digitalWrite(LED_GNSS, LOW);     break;   case ELTRES_BOARD_EVT_GNSS_RECEIVE:     // GNSS電波受信     Serial.println("gnss wave has been received.");     digitalWrite(LED_GNSS, HIGH);     gnss_recevie_timeout = false;     break;   case ELTRES_BOARD_EVT_FAULT:     // 内部エラー発生     Serial.println("internal error.");     break;   } } /**  * @brief GGA情報受信コールバック  * @param gga_info GGA情報のポインタ  */ void gga_event_cb(const eltres_board_gga_info *gga_info) {   // Serial.print("[gga]");   last_gga_info = *gga_info;   if (gga_info->m_pos_status)   {     測位状態     GGA情報をシリアルモニタへ出力     Serial.print("utc: ");     Serial.println((const char *)gga_info->m_utc);     Serial.print("lat: ");     Serial.print((const char *)gga_info->m_n_s);     Serial.print((const char *)gga_info->m_lat);     Serial.print(", lon: ");     Serial.print((const char *)gga_info->m_e_w);     Serial.println((const char *)gga_info->m_lon);     Serial.print("pos_status: ");     Serial.print(gga_info->m_pos_status);     Serial.print(", sat_used: ");     Serial.println(gga_info->m_sat_used);     Serial.print("hdop: ");     Serial.print(gga_info->m_hdop);     Serial.print(", height: ");     Serial.print(gga_info->m_height);     Serial.print(" m, geoid: ");     Serial.print(gga_info->m_geoid);     Serial.println(" m");   }   else   {     非測位状態     "invalid data"をシリアルモニタへ出力     Serial.println("invalid data.");   } } /**  * @brief setup()関数  */ void setup() {   // シリアルモニタ出力設定   Serial.begin(115200);   // LED初期設定   pinMode(LED_RUN, OUTPUT);   digitalWrite(LED_RUN, HIGH);   pinMode(LED_GNSS, OUTPUT);   digitalWrite(LED_GNSS, LOW);   pinMode(LED_SND, OUTPUT);   digitalWrite(LED_SND, LOW);   pinMode(LED_ERR, OUTPUT);   digitalWrite(LED_ERR, LOW);   s_servo_1.attach(PIN_D03);   s_servo_1.write(0);   delay(1000);   String ver = quirc_version();   Serial.println("quirc version: " + ver);   qr_code_str = "NO QR CODE";   Serial.println(theCamera.begin(1, CAM_VIDEO_FPS_15,                                  CAM_IMGSIZE_QVGA_H, CAM_IMGSIZE_QVGA_V, CAM_IMAGE_PIX_FMT_RGB565));   // quirc_destroy(qr);   // ELTRES起動処理   eltres_board_result ret = EltresAddonBoard.begin(ELTRES_BOARD_SEND_MODE_1MIN, eltres_event_cb, gga_event_cb);   if (ret != ELTRES_BOARD_RESULT_OK)   {     // ELTRESエラー発生     digitalWrite(LED_RUN, LOW);     digitalWrite(LED_ERR, HIGH);     program_sts = PROGRAM_STS_STOPPED;     Serial.print("cannot start eltres board (");     Serial.print(ret);     Serial.println(").");   }   else   {     // 正常     program_sts = PROGRAM_STS_RUNNING;   } } /**  * @brief loop()関数  */ void loop() {   bool wasSent = false;   switch (program_sts)   {   case PROGRAM_STS_RUNNING:     // プログラム内部状態:起動中     if (gnss_recevie_timeout)     {       // GNSS電波受信タイムアウト(GNSS受信エラー)時の点滅処理       uint64_t now_time = millis();       if ((now_time - last_change_blink_time) >= 1000)       {         last_change_blink_time = now_time;         bool set_value = digitalRead(LED_ERR);         bool next_value = (set_value == LOW) ? HIGH : LOW;         digitalWrite(LED_ERR, next_value);       }     }     else     {       digitalWrite(LED_ERR, LOW);     }     if (event_send_ready)     {       // 送信直前通知時の処理       event_send_ready = false;       setup_payload_gps();       // 送信ペイロードの設定       EltresAddonBoard.set_payload(payload_for_gnss);     if (!is_set_payload_qr || change_payload)     {       is_set_payload_qr = false;       new_ans_str = setup_payload_random();       change_payload = false;       uint8_t new_payload[16];       memcpy(new_payload, payload_for_qr, sizeof(payload_for_qr));     }       // payloadからnew_payloadにコピー       uint8_t ret = EltresAddonBoard.set_payload(payload_for_qr);       Serial.println();       if (ret == ELTRES_BOARD_RESULT_OK)       {         Serial.println("send_qr_code_num");         is_set_payload_qr = true;         ans_str = new_ans_str;       }     }     break;   case PROGRAM_STS_STOPPED:     // プログラム内部状態:終了     break;   } //payloadがwebに設定されたら   if (is_set_payload_qr)   {     // QRコードの読み取りを開始、 streamを開始指定いなければ     if (!is_camera_streaming)     {       qr = quirc_new();       if (qr == NULL)       {         Serial.println("can't create quirc object");         return;       }       if (quirc_resize(qr, 320, 240) < 0)       {         Serial.println("Failed to allocate video memory");         return;       }       is_camera_streaming = true;       Serial.println("start streaming");       theCamera.startStreaming(true, CamCB);     }     if (qr_code_str.equals(ans_str) )     {       Serial.println("QR CODE MATCHED");       qr_code_str = "NO QR CODE";       s_servo_1.write(90);  // サーボモーターを180度の位置まで動かす       delay(10000);       s_servo_1.write(0);    // サーボモーターを0度の位置まで動かす       delay(1000);       is_camera_streaming = false;       change_payload = true;       // is_set_payload_qr = false;     }   }   // コピーされた配列の内容を表示(確認用)   delay(100); } void setup_payload_gps() {   String lat_string = String((char *)last_gga_info.m_lat);   String lon_string = String((char *)last_gga_info.m_lon);   int index;   uint32_t gnss_time;   uint32_t utc_time;   // GNSS時刻(epoch秒)の取得   EltresAddonBoard.get_gnss_time(&gnss_time);   // UTC時刻を計算(閏秒補正)   utc_time = gnss_time - 18;   // 設定情報をシリアルモニタへ出力   Serial.print("[setup_payload_gps]");   Serial.print("lat:");   Serial.print(lat_string);   Serial.print(",lon:");   Serial.print(lon_string);   Serial.print(",utc:");   Serial.print(utc_time);   Serial.print(",pos:");   Serial.print(last_gga_info.m_pos_status);   Serial.println();   // ペイロード領域初期化   memset(payload_for_gnss, 0x00, sizeof(payload_for_gnss));   // ペイロード種別[GPSペイロード]設定   payload_for_gnss[0] = 0x81;   // 緯度設定   index = 0;   payload_for_gnss[1] = (uint8_t)(((lat_string.substring(index, index + 1).toInt() << 4) + lat_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   payload_for_gnss[2] = (uint8_t)(((lat_string.substring(index, index + 1).toInt() << 4) + lat_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   index += 1; // skip "."   payload_for_gnss[3] = (uint8_t)(((lat_string.substring(index, index + 1).toInt() << 4) + lat_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   payload_for_gnss[4] = (uint8_t)(((lat_string.substring(index, index + 1).toInt() << 4) + lat_string.substring(index + 1, index + 2).toInt()) & 0xff);   // 経度設定   index = 0;   payload_for_gnss[5] = (uint8_t)(lon_string.substring(index, index + 1).toInt() & 0xff);   index += 1;   payload_for_gnss[6] = (uint8_t)(((lon_string.substring(index, index + 1).toInt() << 4) + lon_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   payload_for_gnss[7] = (uint8_t)(((lon_string.substring(index, index + 1).toInt() << 4) + lon_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   index += 1; // skip "."   payload_for_gnss[8] = (uint8_t)(((lon_string.substring(index, index + 1).toInt() << 4) + lon_string.substring(index + 1, index + 2).toInt()) & 0xff);   index += 2;   payload_for_gnss[9] = (uint8_t)(((lon_string.substring(index, index + 1).toInt() << 4) + lon_string.substring(index + 1, index + 2).toInt()) & 0xff);   // 時刻(EPOCH秒)設定   payload_for_gnss[10] = (uint8_t)((utc_time >> 24) & 0xff);   payload_for_gnss[11] = (uint8_t)((utc_time >> 16) & 0xff);   payload_for_gnss[12] = (uint8_t)((utc_time >> 8) & 0xff);   payload_for_gnss[13] = (uint8_t)(utc_time & 0xff);   // 拡張用領域(0固定)設定   payload_for_gnss[14] = 0x00;   // 品質設定   payload_for_gnss[15] = last_gga_info.m_pos_status; } String setup_payload_random() {   srand(time(NULL));   memset(payload_for_qr, 0x00, sizeof(payload_for_qr));   ans_str = "test";   new_ans_str = "";   String hex_string;   uint8_t new_payload;   char hexValue[3];   for (int i = 0; i < 15; i++)   {     // hex_string = random_hex_char();     // new_payload =  (uint8_t)strtol(hex_string.c_str(), NULL, 10);     new_payload = random_hex_char();     Serial.println(new_payload);     payload_for_qr[i + 1] = new_payload;     sprintf(hexValue, "%02x", new_payload);     // ans_str += String(hex_string);     // Serial.print(hex_string);     new_ans_str += String(hexValue);   }   Serial.println();   Serial.print("new_ans_str:");   Serial.println(new_ans_str);   payload_for_qr[0] = 0x8A;   return new_ans_str; } uint8_t random_hex_char() {   int num = rand() % 256;   uint8_t ret = static_cast<uint8_t>(num & 0xff);   // char hexValue;   // // 0パディングで文字列の最小幅は2文字   // sprintf(&hexValue, "%02X", num);   return ret; } void CamCB(CamImage img) {   if (!img.isAvailable())     return;   if (qr_code_str.equals(ans_str))   {     return;   }   bool success = false;   image = quirc_begin(qr, &w, &h);   if (w != CAM_IMGSIZE_QVGA_H || h != CAM_IMGSIZE_QVGA_V)   {     Serial.println("configration error");     while (1)       ; // fatal error to enter the infinite loop (stop process)   }   // copy gray scale image   uint16_t *rgb_image = (uint16_t *)img.getImgBuff();   for (int n = 0; n < w * h; ++n)   {     uint16_t pix = rgb_image[n];     image[n] = (pix & 0x7E0) >> 5; // extract g image   }   quirc_end(qr);   int num_codes = quirc_count(qr);   if(num_codes>0){   Serial.println("num codes: " + String(num_codes));   }   if (num_codes > 0)   {     for (int i = 0; i < num_codes; i++)     {       quirc_decode_error_t err;       quirc_extract(qr, i, &code);       err = quirc_decode(&code, &data);       if (err)         qr_code_str = "DECODE FAILED";       else         qr_code_str = data.payload;         success=true;     }   }   else   {     qr_code_str = "NO QR CODE";   }   if(success){   Serial.println(qr_code_str);   } } ``` ## 外部サーバー 外部サーバーはWebサイトを表示するサーバーどClip Viewer LiteのAPIを叩くAPI サーバーを用意しました。 ### Webサイト QRコードのもとになるキーをAPIサーバーから受けとって、QRコードを作成して表示させる役割を持ちます。 これはVueを用いて作成しました。 #### ソースコード 重要なコンポーネントのソースコードを記載します。`API_URL`の部分は適宜自分自身の作成したAPIサーバーのURLに置き換えてください。`showvideo.vue`を呼びだすことで動画を再生させて、動画を見終わることでQRコードを表示させるシステムを作ることができます。 ```javascript // showvideo.vue <script setup lang="ts"> import { defineProps, ref } from 'vue' import ButtonToApi from './ButtonToApi.vue'; const videoFinished = ref(false); const videoPlayer = ref<HTMLVideoElement | null>(null) const onVideoEnded = () => {   console.log("onVideoEnded")   videoPlayer.value?.play()   videoFinished.value = true; } </script> <template>   <div class = "box">   <div>   <video controls ref="videoPlayer" @ended="onVideoEnded" src="./gomi.mp4"  width="500" height="400" autoplay></video>   </div>   <div v-if="videoFinished">     <!-- ビデオ終了後に表示したい内容 -->     <ButtonToApi/>   </div>   </div> </template> <style scoped> .box {   display: flex;   justify-content: center;   align-items: center;   flex-direction: column;   height: auto; } </style> ``` ```javascript //buttontoapi.vue <script setup lang="ts"> import VueQrcode from "@chenfengyuan/vue-qrcode"; import axios from 'axios'; import { ref } from "vue"; const VUE_APP_API_DOMAIN = API_URL; const qrcodeRaw = ref<string | undefined>(undefined); const option = {     errorCorrectionLevel: "M",     maskPattern: 0,     margin: 10,     scale: 2,     width: 300,     color: {         dark: "#000000FF",         light: "#FFFFFFFF"     } } function formatDate(year: number, month: number, day: number): string {     // 年、月、日をそれぞれ2桁または4桁の文字列に変換     let yearStr = year.toString().padStart(4, '0');     let monthStr = month.toString().padStart(2, '0');     let dayStr = day.toString().padStart(2, '0');     // YYYYMMDD形式の文字列を返す     return yearStr + monthStr + dayStr; } // 関数の使用例 console.log(formatDate(2024, 1, 20)); // 出力: 20240120 const isQRcodeUpdated = ref<boolean>(false); const sendApiOpenBox = async () => {     isQRcodeUpdated.value = false;     const now = new Date();     const year = now.getFullYear();     const month = now.getMonth();     const date = now.getDate();     // 昨日の日付を取得     const fromDate = new Date(year, month, date - 1);     const fromYear = fromDate.getFullYear();     const fromMonth = fromDate.getMonth() + 1;     const fromDay = fromDate.getDate();     // 明日の日付を取得     const toDate = new Date(year, month, date + 1);     const toYear = toDate.getFullYear();     const toMonth = toDate.getMonth() + 1;     const toDay = toDate.getDate();     const from = formatDate(fromYear, fromMonth, fromDay);     const to = formatDate(toYear, toMonth, toDay);     console.log(VUE_APP_API_DOMAIN + `/api/getpayload?from=${from}&to=${to}`);     const data = await axios.get(VUE_APP_API_DOMAIN +`/api/getpayload?from=${from}&to=${to}`);     let isGPSinfo = false;     let isQRinfo = false;     console.dir(data.data);     if (data.data[0] === true) {         if (data.data[1]) {             for(const element of data.data[1]){                 console.log("payloadType:"+element.payloadType);                 if (element.payloadType === "GPS") {                     console.log('find GPS');                     isGPSinfo = true;                 }                 else if (element.payloadType === "フリーコンテナ") {                     console.log('find QR');                     const payloadRandomHex: string = element.payload.substring(2);                     qrcodeRaw.value = payloadRandomHex;                     isQRinfo = true;                     isQRcodeUpdated.value = true;                     break;                 }                 if (isGPSinfo && isQRinfo) {                     break;                 }             };         } else {             console.log("ペイロードが空です");         }     } else {         console.log("error:リクエストが失敗しました");     } } </script> <template>         <div class="box">             <div>                 <button @click="sendApiOpenBox()" class="motion-button">ゴミ箱を開きます</button>             </div>             <transition name="fade">                 <vue-qrcode v-if="isQRcodeUpdated" :value="qrcodeRaw" :options="option" tag="img"></vue-qrcode>             </transition>         </div> </template> <style scoped> button {     background-color: #4CAF50;     border: none;     color: white;     padding: 20px 40px; /* Increase padding for larger button */     text-align: center;     text-decoration: none;     display: inline-block;     font-size: 20px; /* Increase font size for larger button */ } .button-motion-enter-active, .button-motion-leave-active {     transition: opacity 0.5s; } .button-motion-enter, .button-motion-leave-to {     opacity: 0; } .motion-button {     /* Your button styles here */ } .box {     display: flex;     justify-content: center;     align-items: center;     flex-direction: column;     height: auto; } </style> ``` ### API サーバー APIサーバーはNode.js Expressで作成しました。大枠は「PCアプリケーションサンプルプログラム_PythonによるLINE通知連携」を参考にさせていただいて、TypeScriptに書き換えました。Clip Viewer LIteのユーザーネームとパスワードとAPIキーは適宜自分自身のに置き換えてください。 #### ソースコード ```typescript //app.ts const express = require('express'); const axios = require('axios'); require('dotenv').config(); const app = express(); import { Request, Response } from 'express'; const USERNAME = process.env.USERNAME; const PASSWORD = process.env.PASSWORD; const CLIP_API_KEY = process.env.API_KEY; const CLIP_API_ENDPOINT = 'https://api.clip-viewer-lite.com'; const deviceId: string = "0001019947" let token: string; let token_expired: Date = new Date(); //YYYYMMDDT interface DateRange {     from: string;     to: string; } async function getToken(): Promise<boolean> {     const url = `${CLIP_API_ENDPOINT}/auth/token`;     const data = {         username: USERNAME,         password: PASSWORD     };     try {         const response = await axios.post(url, data, {             headers: {'X-API-Key': CLIP_API_KEY}         });         console.log('getToken:');         if (response.status === 200 && response.data.token) {             token = response.data.token;             token_expired = new Date(new Date().getTime() + 4 * 60000); // 4分後             return true;         }         return false;     } catch (error) {         console.error('CLIP API error:', error);         return false;     } } async function retrieveToken(): Promise<boolean> {     let result = false;     const checkTime = new Date();     if (token && checkTime < token_expired) {         result = true;     } else {         result = await getToken();     }     return result; } async function apiGetPayload(deviceId: string,range?:DateRange|undefined): Promise<[boolean, any]> {     let result = false;     let payload: any = null;     let url = "";     try {         await retrieveToken();         if(range !== undefined){             const from = range.from             const to = range.to          url = `${CLIP_API_ENDPOINT}/payload/${deviceId}?from=${from}&to=${to}`;         }else{          url = `${CLIP_API_ENDPOINT}/payload/latest/${deviceId}`;         }         const response = await axios.get(url, {             headers: {                 'X-API-Key': CLIP_API_KEY,                 'Authorization': token,             }         });         console.log('apiGetPayload:');         console.log('url:' + url);         if (response.status === 200) {             const resData = response.data;             const payloadList = resData.payload;             if (payloadList) {                 payload = payloadList;             } else {                 payload = {};             }             result = true;         }     } catch (e) {         console.error("API latest payload error: ", e);     }     return [result, payload]; } async function getPayload(deviceId:string,range:DateRange|undefined): Promise<[boolean, any]> {     let result = false;     let payload: any = null;     const tokenAvailable = await retrieveToken();     if (tokenAvailable) {         if(range === undefined){             [result, payload] = await apiGetPayload(deviceId,undefined);         } else {             [result, payload] = await apiGetPayload(deviceId, range);         }     }     return [result, payload]; } app.get('/getpayload', (req: Request, res: Response) => {     let range: DateRange|undefined = undefined;     if(req.query.from !== undefined && req.query.to !== undefined){         range = {             from:req.query.from as string,             to:req.query.to as string         }     }     getPayload(deviceId,range).then(         (result) => {             res.send(result);         }     ); }) module.exports = app; ``` # 今後の課題 - QRコードを使わず、Spresenseの機械学習における画像分類で蓋を開くタイミングを決定したい - APIサーバーが外部から接続できてしまうので、SSRを導入したい # まとめ 今回は捨てにくいゴミ箱を作成しました。 マイコンボードを私はほとんど初めて触ったのですが、Spresenseにはできないことが少なく比較的自分のやりたいように作成できました。モニター品を頂いて開発できてよかったです。