マイコン温度通知システムの製作4 サーバーとデータベース

前回でLチカができました。

ここから、マイコンからサーバーへデータを送信し、データベースに記録する機能を作っていきます。

Firebaseの設定

GoogleのFirebaseにアクセスし、プロジェクトを作成し、Firestoreでデータベースを作成します(※ 最初は間違えてRealtime Databaseを作成していました)。

データベースの設定をします。

無料の料金プランだとAPIが使えないので有料プランBlazeを設定します。

ローカルPCでFirebase CLIのインストールをします。

npm install -g firebase-tools

適当なディレクトリに移動し、firebase login を実行、Yesと答えて認証します。

$ cd firebase_directory$ firebase login
i  Firebase optionally collects CLI and Emulator Suite usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI and Emulator Suite usage and error reporting information? Yes
i  To change your data collection preference at any time, run `firebase logout` and log in again.

Visit this URL on this device to log in:
ログイン - Google アカウント
Waiting for authentication... ✔ Success! Logged in as xxx@gmail.com

初期化コマンドを入力し、それぞれの質問に回答します。

% firebase init functions

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/user/firebase


=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: temperature-measurement---test (Temperature measurement - test)
i  Using project temperature-measurement---test (Temperature measurement - test)

=== Functions Setup
Let's create a new codebase for your functions.
A directory corresponding to the codebase will be created in your project
with sample code pre-configured.

See https://firebase.google.com/docs/functions/organize-functions for
more information on organizing your functions using codebases.

Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
✔  Wrote functions/package.json
✔  Wrote functions/.eslintrc.js
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes
added 588 packages, and audited 589 packages in 58s

66 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

これで指定のディレクトリ配下にあるindex.jsを修正します。

const {onRequest} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");

// 必要に応じてFirebase Admin SDKを初期化します
// この行は、まだ初期化されていない場合にのみ必要です
if (admin.apps.length === 0) {
  admin.initializeApp();
}

// 新しい関数の定義
exports.addMessage = onRequest(async (req, res) => {
  // リクエストからデータを抽出
  const text = req.query.text;
  // 現在の日時を取得
  const timestamp = new Date().toISOString();

  // データと日時をFirebaseデータベースに保存
  const writeResult = await admin.firestore().collection("messages")
      .add({text: text, timestamp: timestamp});

  // クライアントにレスポンスを送信
  res.json({result: `Message with ID: ${writeResult.id} added.`});
});

ターミナルで作成した関数をFirebaseにデプロイします。

$ firebase deploy --only functions

=== Deploying to 'temperature-measurement---test'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint

> lint
> eslint .

✔  functions: Finished running predeploy script.
i  functions: preparing codebase default for deployment
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
i  artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
✔  functions: required API cloudbuild.googleapis.com is enabled
✔  functions: required API cloudfunctions.googleapis.com is enabled
✔  artifactregistry: required API artifactregistry.googleapis.com is enabled
i  functions: Loading and analyzing source code for codebase default to determine what to deploy
Serving at port 8504

i  functions: preparing functions directory for uploading...
i  functions: packaged /Users/user/program/firebase/functions (146.31 KB) for uploading
i  functions: ensuring required API run.googleapis.com is enabled...
i  functions: ensuring required API eventarc.googleapis.com is enabled...
i  functions: ensuring required API pubsub.googleapis.com is enabled...
i  functions: ensuring required API storage.googleapis.com is enabled...
✔  functions: required API eventarc.googleapis.com is enabled
✔  functions: required API pubsub.googleapis.com is enabled
✔  functions: required API run.googleapis.com is enabled
✔  functions: required API storage.googleapis.com is enabled
i  functions: generating the service identity for pubsub.googleapis.com...
i  functions: generating the service identity for eventarc.googleapis.com...
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 18 (2nd Gen) function xxx(us-central1)...
✔  functions[xxx(us-central1)] Successful update operation.
Function URL (xxx(us-central1)): https://xxx-pfv42ema6a-uc.a.run.app
i  functions: cleaning up build files...

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/temperature-measurement---test/overview

Firebaseのサイトでデプロイした関数を確認できました。

ここからリクエストのURLを確認します。

例:https://[region]-[project-id].cloudfunctions.net/[function-name]

ここからが大変でした。

micropythonの送信プログラムでエラーが頻発していました。

import machine
import onewire
import ds18x20
import urequests
from machine import Pin
import time
import network
import ubinascii

# LEDを点灯
def light_on(pin_num):

  # LEDを接続
  led = Pin(pin_num, Pin.OUT)

  # LEDをONにする
  led.value(1)

  # GPIOピンを出力モードに設定し、内部プルダウン抵抗を有効化
  led = Pin(pin_num, Pin.OUT, Pin.PULL_DOWN)

# LEDを点灯
def light_off(pin_num):
  # LEDを接続
  led = Pin(pin_num, Pin.OUT)

  # LEDをONにする
  led.value(0)

  # GPIOピンを出力モードに設定し、内部プルダウン抵抗を有効化
  led = Pin(pin_num, Pin.OUT, Pin.PULL_DOWN)

# 温度を計測
def read_temperature(pin_num):
  # ESP32のデータピンを設定(例:GPIO 4)
  dat_pin = machine.Pin(pin_num)

  # 1-Wireバスを初期化
  ds_sensor = ds18x20.DS18X20(onewire.OneWire(dat_pin))

  # センサーのロムコードを取得
  roms = ds_sensor.scan()
  print('Found DS18B20 sensors:', roms)

  ds_sensor.convert_temp()
  for rom in roms:
    temperature = ds_sensor.read_temp(rom)
    print("rom :",rom)
    print("temperature :", temperature, "度")
  return temperature

# マイコンのMACアドレスを取得
def get_unique_id():
  mac = network.WLAN().config('mac')
  unique_id = ubinascii.hexlify(mac).decode()
  print("unique_id :",unique_id)
  return unique_id

# Firebase関数にデータを送信する関数
def send_data_to_firebase(temperature, mac_id):

  # Wi-Fiの設定
  ssid = 'xxx'
  password = 'xxx'
  # Wi-Fiステーションモードを有効化
  station = network.WLAN(network.STA_IF)
  station.active(True)
  station.connect(ssid, password)

  # 接続が完了するまで待機
  while not station.isconnected():
      pass

  print('Wi-Fiに接続しました')

  # Firebase関数のURLをここに入力
  url = "https://...."
  headers = {'Content-Type': 'application/json'}
  payload = {'temperature': temperature, 'macAddress': mac_id}
  
  print("Sending payload:", payload)

  try:
    response = urequests.post(url, json=payload, headers=headers)
    print("Data sent. Response:", response.json())
  except Exception as e:
    print("Error sending data:", e)

# MACアドレスを取得
mac_id = get_unique_id()
print("mac_id type :", type(mac_id))
print("mac_id :", mac_id)

# LEDを点灯
light_on(14)

# 温度を読み取る
temperature = read_temperature(25)
print("temperature type :", type(temperature))
print("temperature :", temperature)

# データ送信
send_data_to_firebase(temperature, mac_id)

# 待つ
time.sleep(1)

# LEDをOFFにする
light_off(14)

・エラー情報

$ ampy --port /dev/tty.usbserial-0001 run /Users/user/micropython/main.py
unique_id : 08d1f9e822ec
mac_id type : <class 'str'>
mac_id : 08d1f9e822ec
Found DS18B20 sensors: [bytearray(b'(\xf5\tC\xd4\xe1<7')]
rom : bytearray(b'(\xf5\tC\xd4\xe1<7')
temperature : 25.0 度
temperature type : <class 'float'>
temperature : 25.0
Wi-Fiに接続しました
Sending payload: {'macAddress': '08d1f9e822ec', 'temperature': 25.0}
Data sent. Response: {'error': 'Invalid input: Temperature is not a number'}

どうも送る温度データtemperatureの形式が想定通りの数値になっていないため、エラーになっているようです。

postmanをインストールしてきちんとデータが送信されるか確認します。

ここで以下のように設定してsendして確認します。

  • リクエストのタイプをPOSTに設定
  • URLフィールドにFirebase関数のURLを入力
  • リクエストのヘッダーセクションにContent-Typeとしてapplication/jsonを追加
  • rawタイプを選択して、JSON形式を選ぶ
  • JSONオブジェクトを以下のように入力

そこでsendを押下し送信します。

すると以下のエラーが出ました。

{
    "error": "7 PERMISSION_DENIED: Cloud Firestore API has not been used in project temperature-measurement---test before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?xxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry."
}

これはGoogle Cloud のFirestore APIが有効でないため発生しているエラーらしいので、記載されたURLにアクセスしてAPI有効にします。

API有効後もpostmanで以下のエラーが出ました。

{
    "error": "5 NOT_FOUND: "
}

認証を実施

Firebaseのプロジェクトの設定からサービスアカウントを選択し、示されたコードをコピーします。

「新しい秘密鍵を生成」をクリックし、jsonファイルをダウンロード、コードに指定します。

以下のコードをPythonプログラムに追加し、Firebaseの認証を行います。

import requests
import firebase_admin
from firebase_admin import credentials

cred = credentials.Certificate("/Users/user/program/temperature-measurement---test-firebase-adminsdk-h1irq-e903f7c425.json")
firebase_admin.initialize_app(cred)

firebase_admin をインストールしようとするとエラーが出ました。

conda install -c conda-forge firebase-admin

Channels:
- conda-forge
- defaults
Platform: osx-arm64
Collecting package metadata (repodata.json): done
Solving environment: failed

PackagesNotFoundError: The following packages are not available from current channels:

- firebase-admin

Current channels:

- https://conda.anaconda.org/conda-forge
- defaults

To search for alternate channels that may provide the conda package you're
looking for, navigate to

Just a moment...
and use the search bar at the top of the page.

ARMのMacについて、Anacondaが対応していないためエラーになっているようです。

pip3 install firebase-admin

上記コマンドを実行しインストールしてもダメだったので調べたところ、Anaconda Navigatorから設定できるようです。

Environmentsからターミナルを起動します。

ここで

$ conda list firebase-admin
$ pip list | grep firebase-admin
$ pip3 list | grep firebase-admin

でfirebase-adminがインストールされているか確認しますが、インストールされていませんでした。

インストールを試みます。

$ conda install -c conda-forge firebase-admin
$ pip3 install firebase-admin

condaコマンドはfirebase-adminパッケージがAnacondaの現在のチャンネルで利用可能でないためできませんでした。

pip3のコマンドは実行でき、インストールができました。

$ pip3 list | grep firebase-admin
firebase-admin 6.2.0

そこでPythonプログラムを実行したところ、前回と違うエラーが出ました。

・前回のエラー

Data sent. Response: {'error': 'Invalid input: Temperature is not a number'}

・今回のエラー

Data sent. Response: {'error': '5 NOT_FOUND: '}

URLの指定に誤りがあったようです。
コードで記載したCloud Run URLをfuncitonデプロイ時に出力された、

Project Console: https://console.firebase.google.com/project/xxxx

に変更したところ、以下のようにログが変わりました。

Sending payload: {'temperature': 29.5, 'macAddress': '08d1f9e822ec'}
Error sending data: Expecting value: line 1 column 1 (char 0)

Node.jsの未インストールが原因かと思いインストールしても変わりませんでした。

conda install -c conda-forge nodejs

ここで、Firebaseのエミュレータを試してみます。

firebase emulators:start --only functions

・エミュレータの起動ログ

✔ functions: Loaded functions definitions from source: xxx.
✔ functions[us-central1-xxx]: http function initialized (http://127.0.0.1:5001/xxx/us-central1/xxx).

上記より取得したCloud Run URL、「http://127.0.0.1:5001/xxx/us-central1/xxx」をPythonプログラムに追加します。それで試すと、

Sending payload: {'temperature': 29.5, 'macAddress': '08d1f9e822ec'}
Data sent. Response: {'error': 'error: 7, 7 PERMISSION_DENIED: Cloud Firestore API has not been used in project xxx before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=temperature-measurement-b3fcd then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.'}

というエラーが出ました。ここでAPIの設定を行ってリトライします。

Sending payload: {'temperature': 29.5, 'macAddress': '08d1f9e822ec'}
Data sent. Response: {'error': 'error: 5, 5 NOT_FOUND: The database (default) does not exist for project temperature-measurement-b3fcd Please visit https://console.cloud.google.com/datastore/setup?project=xxx to add a Cloud Datastore or Cloud Firestore database. '}

データベースがないというエラーが出たので、エラー記載のURLに遷移し、Cloud Datastoreの設定をします。

Sending payload: {'temperature': 29.5, 'macAddress': '08d1f9e822ec'}
Data sent. Response: {'result': 'Temperature data with ID: ZdKleB3JTcmZkUmUpqSB added.'}

すると上記のように意図するように書き込まれたようです。

Cloud Run URLについてはコードの記載で2種類あり、ここからわかることは、実際にデプロイした関数についてはURLが間違っていたようです。

しかし、エミュレータのUIにアクセスするとデータベースが作成されておらず、見ることができませんでした。

調べると、「firebase emulators:start –only functions」のコマンドでFunctions Emulatorのみを起動してしまっているのが原因のようです。以下のコマンドでエミュレータを起動すると問題は解決しました。

firebase emulators:start

・データベースの指定の間違い

constwriteResult=awaitadmin.firestore()

関数のプログラムで上記のようにFirestoreデータベースを指定しているのに、デバッグ時はRealtime Databaseの画面を注視していました。そのため見当違いなデバッグをしてしまっていました。

・Firestoreデータベースが存在せず書き込みできなかった
デプロイする関数でFirestoreデータベースを指定しているのに、Firestoreのデータベースを作っておらず、存在しないのでエラーとなっていました。そのため、Google Cloudから作成しました。
・リクエストURLの誤解
普通に関数(Function)の画面に表示されるURLがリクエストURLでした。他で調べた内容を混同して、プロジェクトIDを含めるなど誤解をしていました。

誤:https://[region]-[project-id].cloudfunctions.net/[function-name]
正:https://xxx-uc.a.run.app(Function画面で確認)

Cloud Runにはバージョンが2つあり、以下のコード記述だと、「https://xxx.a.run.app(Firebase Functionsバージョン2)」になります。

const {onRequest} = require("firebase-functions/v2/https");

以下のコード記述だと、「https://us-central1-xxx.cloudfunctions.net/xxx(Firebase Functionsバージョン1)」になります。

const functions = require("firebase-functions");
以下のコードを含むプログラムをデプロイするとポートが違ったりCloud Runを使用するときに違うポートを使用してしまい、正常にデプロイされず、意図する日本時間のタイムスタンプが生成されませんでした。
const moment = require("moment-timezone");

そのエラーは以下になります。

$ firebase deployi functions: Loading and analyzing source code for codebase default to determine what to deploy
Serving at port 8225i functions: updating Node.js 18 (2nd Gen) function addTemperatureData(us-central1)...
Could not create or update Cloud Run service addtemperaturedata, Container Healthcheck failed. Revision 'addtemperaturedata-00003-geb' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable. Logs for this revision might contain more information.Logs URL: https://console.cloud.google.com/...
For more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-startFunctions deploy had errors with the following functions:
xxx(us-central1)
i functions: cleaning up build files...Error: There was an error deploying functions
moment-timezoneライブラリがプロジェクトにインストールされていないらしいので、プロジェクトのルートディレクトリで以下を実施しました。
npm install moment-timezone --save
npm uninstall moment-timezone
npm install moment-timezone
rm -rf node_modules
rm package-lock.json
npm install
これを実施した後、デプロイが上手くいきました。
(追記:しかしこれは誤りで、Firebase直下でインストールやFirebase直下のpackage.jsonファイルに記載をしており、正しくはFunctions配下でのインストールやpackage.jsonファイルへの記載をすべきでした)
・エミュレータを起動してもそのFirestoreデータベースがONにならない
最初の「firebase init」コマンドでエミュレータの設定を選択できるが、それを実施していなかったのが原因でした。
また、javaがインストールされていなかったのでインストールしました。
・エミュレータを起動してもエミュレータUIが起動しない
emulators: The Emulator UI is not starting, either because none of the running emulators have a UI component or the Emulator UI cannot determine the Project ID. Pass the --project flag to specify a project.

上記のようなエラーが出ていました。

これはfirebase.jsonの”emulators” の”enabled”: falseとなっていたのが原因で、”enabled”: true とすると起動できました。
・firebase.json
  "emulators": {
    "ui": {
      "enabled": true
    },
・エミュレータを起動すると以下のエラーが出る
$ firebase emulators:start
i emulators: Starting emulators: functions, firestore, database, hosting, extensions
⚠ firestore: Port 8080 is not open on localhost (127.0.0.1,::1), could not start Firestore Emulator.
⚠ firestore: To select a different host/port, specify that host/port in a firebase.json config file:
{
// ...
"emulators": {
"firestore": {
"host": "HOST",
"port": "PORT"
}
}
}
i emulators: Shutting down emulators.
⚠ ui: Emulator UI unable to start on port 4000, starting on 4001 instead.
⚠ hosting: Hosting Emulator unable to start on port 5000, starting on 5002 instead.Error: Could not start Firestore Emulator, port taken.

ポート8080が使用中なので以下のコマンドで確認します。

lsof -i :8080

例えば、java 93818 と表示されたら、

kill 93818

と入力してポートを開放します。

・サーバーがUSAにあるため、向こうの時間でタイムスタンプを取得してしまう?
“2023-11-27T13:50:48.128Z”など、アメリカの基準時になってしまっていました。
また、サーバーからデバイスへの応答で時刻を返しても、今度は日本時間になってしまい、サーバーに保存される日時と応答の日時が違っていました。
色々と試したが、どうも上手くいきませんでした。
日時プラス9時間など修正してもデータベースに保存されるときは意図しない日時になったり、それどころか、以下のコードで保存しようとしたら9時間未来の時刻になっていました。
const timestamp = moment().tz("UTC").format("YYYY-MM-DD HH:mm:ss");
(日本時間11/29 10:00に実行して、記録されるのが 11/29 19:00 UTC+9)
ネットで検索したところ同じ現象の記載があり、原因はサーバーの設定でした。
Google Cloud PlattformでCloud Functionより、関数を選択し、「編集」より「ランタイム、ビルド、接続、セキュリティの設定」から、環境変数(名:TZ 値:Asia/Tokyo)を設定することで修正できました。
おそらく、サーバー側のタイムゾーンがアメリカなのに時刻だけ日本のものが送られてきて、それがアメリカのタイムゾーンでその時間となり、日本時間に直すとおかしなことになったのでしょう。
また、以下のようにソースで環境変数を設定します。
// タイムゾーン環境変数を指定
const timezone = 'Asia/Tokyo';
process.env.TZ = timezone;

これでおおよそができたので、今度はデータベースのデータを表示するアプリケーションを作っていきます。

タイトルとURLをコピーしました