PythonのBaseHTTPRequestHandlerを使用した温度センサ(BMP280)のデータの受け取りと管理

## 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秒間隔でデータを送信（適宜調整）
       

### センサの配線図![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562116/eb81be81-9a9c-afd7-8670-0ed4918d1518.png)

### センサのモジュールコード

このコードは[BMP280のホームページ](https://www.mouser.jp/ProductDetail/Bosch-Sensortec/BMP280?qs=QhAb4EtQfbUP9Z%252BCHM3Wyg%3D%3D&srsltid=AfmBOoodGtl3y_fFG43S70I-G2n_FcIwfIMmkJgl-CpHZBC2GE18Uyc9)にあるデータシートの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とアドレス別による管理を行いました. 
不出来なプログラムですが, 実験や実装の助けになればと思います.