1. はじめに
私が研究で用いたセンサのデータをJSON形式でHTTP POSTリクエストとして送信し, BaseHTTPRequestHandlerを使用してサーバで受信し管理した方法をまとめたものです.
BaseHTTPRequestHandler とは
Python の標準ライブラリ http.server モジュールに含まれているクラスで、HTTPリクエストを処理するための基本的なフレームワークを提供します。このクラスを拡張して独自のHTTPサーバーを構築することができます。
2. 構成
サーバーを開設した後, クライアントからhttpリクエストを送信します. サーバーはクライアントからのリクエストを正常に受け取り, 処理し, HTTPステータスコード200を含むレスポンスを返します. これにより, ESP32はリクエストが成功したことを確認できます.
サーバー構成:
● BaseHTTPRequestHandlerを使用
● 言語:Python ver.3.12.0
● Ubuntu 24.04 LTS
クライアント構成:
● マイクロコントローラ:ESP32
● 言語:MicroPython
● ファームウェア:ESP32_GENERIC-20240222-v1.22.2
●センサ:BMP280(高精度気圧・温度センサーモジュール)
#クライアントプログラムはMicroPythonで記述しているためPython環境では動作しない場合があります。
3. サーバープログラム
設定箇所
・SERVER_IP:扱うWi-Fiのアドレスを記入
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import csv
# VMのIPアドレスとポート番号
VM_IP = '' # VMの実際のIPアドレスに置き換える
VM_PORT = 8000
class DataHandler(BaseHTTPRequestHandler):
device_data = {}
def write_csv_header(self, csv_file, data):
"""CSVファイルにヘッダーを書き込む"""
headers = []
if 'temperature' in data:
headers.append('Temperature (C)')
if 'pressure' in data:
headers.append('Pressure (Pa)')
with open(csv_file, mode='w', newline='') as file:
csv.writer(file).writerow(headers)
def append_csv_data(self, csv_file, data):
"""CSVファイルにデータを書き込む"""
row = []
if 'temperature' in data:
row.append(data['temperature'])
if 'pressure' in data:
row.append(data['pressure'])
with open(csv_file, mode='a', newline='') as file:
csv.writer(file).writerow(row)
def do_POST(self):
# リクエストデータを取得
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
client_ip = self.client_address[0]
# クライアントごとのCSVファイルを準備
if client_ip not in self.device_data:
csv_file = f"temperature_data_{client_ip.replace('.', '_')}.csv"
self.write_csv_header(csv_file, data)
self.device_data[client_ip] = csv_file
csv_file = self.device_data[client_ip]
# データをCSVに保存
self.append_csv_data(csv_file, data)
# 応答を返す
self.send_response(200)
self.end_headers()
self.wfile.write("Data received and saved successfully".encode('utf-8'))
#HTTPサーバーの設定と起動
def run_server():
server_address = (VM_IP, VM_PORT)
httpd = HTTPServer(server_address, DataHandler)
print(f'Starting HTTP server on {VM_IP}:{VM_PORT}...')
httpd.serve_forever()
if __name__ == '__main__':
run_server()
4. クライアントプログラム
設定箇所
・SSID:Wi-Fiの名前
・PASSWORD:Wi-Fiのパスワード
・SERVER_IP:Wi-FiのIPアドレス(VM環境であればホストネーム)
・i2c = machine.I2C…:適応するSCL, SDAのPin番号を記載(本稿ではSCL=22, SDA=21 で設定)
~補足~
下記の部分はESP32内臓LEDをwi-fi接続時に点灯を行い処理を可視化するために使用している
#ピン設定
led_blue = machine.Pin(2, Pin.OUT)
import network
import urequests
import machine
import time
import json
from machine import Pin, SoftI2C
# WiFi接続情報の読み込み
SSID = ""
PASSWORD = ""
#モジュールの読み込み
execfile(f"BMP280_Module.py")
# VMのIPアドレスとポート番号
VM_IP = '' # VMの実際のIPアドレスに置き換える
VM_PORT = 8000
# BMP280センサの初期化
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))
init_bmp280(i2c)
count = 0
led_blue = machine.Pin(2, Pin.OUT)
# WiFiに接続する関数
def connect_wifi(ssid, passkey, timeout=10):
wifi = network.WLAN(network.STA_IF)
if wifi.isconnected():
print('WiFiに接続済み。接続をスキップします。')
return wifi
else:
wifi.active(True)
wifi.connect(ssid, passkey)
while not wifi.isconnected() and timeout > 0:
print('.')
time.sleep(1)
timeout -= 1
if wifi.isconnected():
print('WiFiに接続しました')
led_blue.value(1)
return wifi
else:
print('WiFiへの接続に失敗しました')
return None
# データをVMに送信する関数
def send_data_to_vm(temperature, pressure):
url = f'http://{VM_IP}:{VM_PORT}'
headers = {'Content-Type': 'application/json'}
data = {
"temperature": temperature,
"pressure": pressure
}
response = urequests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
#print('データの送信に成功しました')
return
else:
print(f'データの送信に失敗しました: {response.status_code}')
response.close()
# メインプログラムの実行部分
print('WiFiに接続します...')
wifi = connect_wifi(SSID, PASSWORD)
if wifi:
while True:
try:
temp = str(read_temp(i2c))
except:
temp = "None"
try:
pres = str(read_pressure(i2c))
except:
pres = "None"
print(f'count: {count+1}\n Temperature: {temp} °C,\n Pressure: {pres} Pa')
try:
send_data_to_vm(temp, pres)
except Exception as e:
print("送信失敗")
time.sleep(5) # 5秒間隔でデータを送信(適宜調整)
センサの配線図
センサのモジュールコード
このコードはBMP280のホームページにあるデータシートのPDFに記載してあるC言語のコードをMicroPythonで動作するようにしたコードです
また、実験で扱うために『def read_temp』と『def read_pressure』で別々の関数で温度と気圧データを取得できるように変更を加えました
※使用する場合ファイル名をBMP280_Module.pyに設定してください。
import machine
#execfile("bmp280_module.py")
# BMP280のアドレス
BMP280_I2C_ADDRESS = 0x76
# BMP280のレジスタ
BMP280_REG_TEMP_XLSB = 0xFC
BMP280_REG_TEMP_LSB = 0xFB
BMP280_REG_TEMP_MSB = 0xFA
BMP280_REG_PRESS_XLSB = 0xF9
BMP280_REG_PRESS_LSB = 0xF8
BMP280_REG_PRESS_MSB = 0xF7
BMP280_REG_CONFIG = 0xF5
BMP280_REG_CTRL_MEAS = 0xF4
BMP280_REG_STATUS = 0xF3
BMP280_REG_RESET = 0xE0
BMP280_REG_ID = 0xD0
# BMP280の設定
BMP280_OSRS_T = 1 # Temperature oversampling x 1
BMP280_OSRS_P = 1 # Pressure oversampling x 1
BMP280_MODE = 3 # Normal mode
# 設定の計算
ctrl_meas = (BMP280_OSRS_T << 5) | (BMP280_OSRS_P << 2) | BMP280_MODE
# グローバル変数
calibration_params = {}
def init_bmp280(i2c):
global calibration_params
# BMP280の初期化
i2c.writeto_mem(BMP280_I2C_ADDRESS, BMP280_REG_CTRL_MEAS, bytes([ctrl_meas]))
# 補正パラメータの読み取り
#read_calibration_paramsを別ファイルに置く場合
#data = i2c.readfrom_mem(BMP280_I2C_ADDRESS, 0x88, 24)
#calibration_params = read_calibration_params(data)
def read_calibration_params():
data = i2c.readfrom_mem(BMP280_I2C_ADDRESS, 0x88, 24)
dig_T1 = data[1] << 8 | data[0]
dig_T2 = data[3] << 8 | data[2]
dig_T3 = data[5] << 8 | data[4]
dig_P1 = data[7] << 8 | data[6]
dig_P2 = data[9] << 8 | data[8]
dig_P3 = data[11] << 8 | data[10]
dig_P4 = data[13] << 8 | data[12]
dig_P5 = data[15] << 8 | data[14]
dig_P6 = data[17] << 8 | data[16]
dig_P7 = data[19] << 8 | data[18]
dig_P8 = data[21] << 8 | data[20]
dig_P9 = data[23] << 8 | data[22]
return {
'dig_T1': dig_T1,
'dig_T2': dig_T2,
'dig_T3': dig_T3,
'dig_P1': dig_P1,
'dig_P2': dig_P2,
'dig_P3': dig_P3,
'dig_P4': dig_P4,
'dig_P5': dig_P5,
'dig_P6': dig_P6,
'dig_P7': dig_P7,
'dig_P8': dig_P8,
'dig_P9': dig_P9,
}
calibration_params = read_calibration_params()
def read_temp(i2c):
data = i2c.readfrom_mem(BMP280_I2C_ADDRESS, BMP280_REG_PRESS_MSB, 6)
# 温度
adc_T = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
var1 = ((((adc_T >> 3) - (calibration_params['dig_T1'] << 1))) * (calibration_params['dig_T2'])) >> 11
var2 = (((((adc_T >> 4) - (calibration_params['dig_T1'])) * ((adc_T >> 4) - (calibration_params['dig_T1']))) >> 12) * (calibration_params['dig_T3'])) >> 14
t_fine = var1 + var2
temperature = (t_fine * 5 + 128) >> 8
if var1 == 0:
return None
return temperature / 100.0
def read_pressure(i2c):
data = i2c.readfrom_mem(BMP280_I2C_ADDRESS, BMP280_REG_PRESS_MSB, 6)
adc_T = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
var1 = ((((adc_T >> 3) - (calibration_params['dig_T1'] << 1))) * (calibration_params['dig_T2'])) >> 11
var2 = (((((adc_T >> 4) - (calibration_params['dig_T1'])) * ((adc_T >> 4) - (calibration_params['dig_T1']))) >> 12) * (calibration_params['dig_T3'])) >> 14
t_fine = var1 + var2
# 気圧
adc_P = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
var1 = t_fine - 128000
var2 = var1 * var1 * calibration_params['dig_P6']
var2 = var2 + ((var1 * calibration_params['dig_P5']) << 17)
var2 = var2 + (calibration_params['dig_P4'] << 35)
var1 = ((var1 * var1 * calibration_params['dig_P3']) >> 8) + ((var1 * calibration_params['dig_P2']) << 12)
var1 = (((1 << 47) + var1) * calibration_params['dig_P1']) >> 33
if var1 == 0:
return None
p = 1048576 - adc_P
p = (((p << 31) - var2) * 3125) // var1
var1 = (calibration_params['dig_P9'] * (p >> 13) * (p >> 13)) >> 25
var2 = (calibration_params['dig_P8'] * p) >> 19
pressure = ((p + var1 + var2) >> 8) + (calibration_params['dig_P7'] << 4)
return pressure / 25600.0
5. 実行結果
一部アドレス等の部分を除き結果を表示します
クライアントとサーバのプログラムを実行します。仮に接続ができていない場合、count1 のように送信失敗という結果が表示されます。接続ができていれば何も表示されません。(count2 からは正常)
これは5秒おきにデータを送信しました。
クライアント
MPY: soft reboot
WiFiに接続します...
.
.
WiFiに接続しました
count: 1
Temperature: 26.83 °C,
Pressure: 1017.922 Pa
送信失敗
count: 2
Temperature: 26.84 °C,
Pressure: 1017.431 Pa
count: 3
Temperature: 26.86 °C,
Pressure: 1016.998 Pa
count: 4
Temperature: 26.86 °C,
Pressure: 1016.45 Pa
count: 5
Temperature: 26.87 °C,
Pressure: 1016.148 Pa
count: 6
.
.
.
サーバー
[サーバーIPアドレス]:サーバーのIPアドレスが表示されます
[ポート番号]:設定しているポート番号が表示されます
[クライアントIPアドレス]:クライアント機器のIPアドレスが表示されます。機器の識別に用いられます
このようにサーバー側では正常に受信できており、5秒間隔にデータの取得が出来ていることが分かります
Starting HTTP server on [サーバーIPアドレス]:[ポート番号]...
[クライアントIPアドレス] - - [22/Nov/2024 06:20:44] "POST / HTTP/1.0" 200 -
[クライアントIPアドレス] - - [22/Nov/2024 06:20:49] "POST / HTTP/1.0" 200 -
[クライアントIPアドレス] - - [22/Nov/2024 06:20:54] "POST / HTTP/1.0" 200 -
[クライアントIPアドレス] - - [22/Nov/2024 06:20:59] "POST / HTTP/1.0" 200 -
[クライアントIPアドレス] - - [22/Nov/2024 06:21:04] "POST / HTTP/1.0" 200 -
[クライアントIPアドレス] - - [22/Nov/2024 06:21:09] "POST / HTTP/1.0" 200 -
サーバーで受け取ったセンサーデータ, 温度データ(temp)と気圧データ(press)をクライアントのIPアドレスごとにcsvファイルとして保存しています.
仮に、別のクライアント機器で同じように実行した場合別のCSVファイルが生成され個別に保管されます。(複数の機器を同時に実行することもできます)
Temperature (C),Pressure (Pa)
26.83,1017.922
26.84,1017.431
26.86,1016.998
26.86,1016.45
26.87,1016.148
26.87,1015.931
6. おわりに
今回 BaseHTTPRequestHandlerを使用してセンサーデータの取得とcsvとアドレス別による管理を行いました.
不出来なプログラムですが, 実験や実装の助けになればと思います.