1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ADPiとアナログ距離センサでの定期計測:Raspberry Piメタルケースセットの活用法

Last updated at Posted at 2025-06-02

はじめに

今回は弊社製品のメタルケースセット(ADPi Pro + slee-Pi 3)を使用し、アナログ出力型の測距モジュール GP2Y0A21YK で計測した距離を、ラズパイ上のWebサーバーにChart.jsを使用し、リアルタイムで表示する方法をご紹介します。

メタルケースセット(ADPi Pro + slee-Pi 3)には、Raspberry Pi 4 Model Bと、高精度AD変換モジュールのADPi Proと、電源管理や時刻の保持が可能なslee-Pi 3が搭載されています。さらに、環境構築済みSDカードやACアダプタ等も付属しており、すぐにお使いいただくことが可能です。

使うもの

image.png

image.png

ハードウェアの準備

image.png
今回はADPi ProのCH1に測距モジュールを接続します。メタルケースセットのカバーを外し、CH1のターミナルを外します。

image.png
図の向きのとき、上から12V、CH1+、CH1-、GNDとなっています。測距モジュールのデータシートを参考に接続してください。また、CH1-とGNDは用意した配線で短絡し、12VとGNDの間には10μFのコンデンサを接続します。

image.png
図を参考にADPi ProのCH1にターミナルを取り付け、付属の「DCジャック - XHP-2コネクタ変換ハーネス」をslee-pi 3のCN1に取り付け、付属のACアダプタから電源を投入します。

ソフトウェアの準備

電源を投入し、ラズパイが起動したらPCからログインしてください。初期ログイン情報はこちらを参照してください。ラズパイへのログイン方法についてはこちらなどを参照してください。
ログインができたら、PCと同一のLAN内にラズパイを接続してください。

任意のディレクトリに移動し、以下の2つのスクリプト(get_adpi.py と web_server.py)を作成します。今回は、/home/mtx/adpi-web-monitorに作成しました。ディレクトリを変更する場合はそれぞれのスクリプトのDIR=も変更してください。

get_adpi.py
import spidev
import smbus
import adpi
import sys
import time
import csv
import os
from datetime import datetime
from time import sleep

DIR = "/home/mtx/adpi-web-monitor"

INTERVAL_SEC = 1 # 計測のインターバル(秒)
ADPI_CH = "1" # 使用するADPiのチャンネル

RAW_OFFSET = (1 << 23) # AD7794の24bit ADC出力(2の補数形式)で0Vに相当する値(=8388608)
RAW_SCALE = (
    0.000596040,
    0.000298020,
    0.000149010,
    0.000074500,
    0.000037250,
    0.000018620,
    0.000009310,
    0.000004650,
) # AD7794のゲインごとの1bitあたりの電圧換算係数(V/LSB)
TEMP_VREF = 1.17 # AD7794内部温度センサ用の基準電圧(V)

def v2k(rate, val):
    for k, v in rate.items():
        if v == val:
            return k

def set_calib(dev):
    g = dev.adc.gain['1']
    r = dev.adc.rate['470']
    bias = dev.load_bias(g)
    scale = dev.load_scale(g)
    _, r = dev.read_mode()
    for i in range(dev.channels):
        dev.write_configuration(g, i)
        dev.write_mode(dev.adc.mode['idle'], r)
        dev.write_offset(bias[i])
        dev.write_mode(dev.adc.mode['idle'], r)
        dev.write_fullscale(scale[i])

def single_conversion(dev, ch):
    c = dev.adc.channel[ch]
    g, _ = dev.read_configuration()
    dev.write_configuration(g, c)
    _, r = dev.read_mode()
    dev.write_mode(dev.adc.mode['single'], r)
    rate = v2k(dev.adc.rate, r)

    while True:
        sleep(2 * 1.0 / float(rate))
        if not dev.read_status() & 0x80:
            break

    raw = dev.read_data()
    return raw, g

def get_voltage(dev, ch):
    raw, g = single_conversion(dev, ch)
    vol = RAW_SCALE[g] * (raw - RAW_OFFSET)
    return vol

def calc_distance(v):
    d = 22 / (v - 0.13) * 1000
    return d

def ctrl_output(dev, ch, state):
    c = dev.adc.channel[ch]
    dev.set_output(c, state)

if __name__ == "__main__":
    spibus = 0
    spics = 0
    eeprombus = 1
    eepromaddr = 0x57
    gpiobus = 1
    gpioaddr = 0x27
    spi = spidev.SpiDev()
    i2c = smbus.SMBus(eeprombus)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    csv_path = f"{DIR}/sensor_data_{timestamp}.csv"

    try:
        spi.open(spibus, spics)
        spi.mode = 0b11
        spi.max_speed_hz = 1000000
        ad = adpi.ADPiPro(spi, i2c, eepromaddr, gpioaddr)
        ctrl_output(ad, ADPI_CH, True)
        set_calib(ad)
        sleep(0.5)

        with open(csv_path, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["timestamp", "distance[cm]"])

        link_path = f"{DIR}/sensor_data_latest.csv"
        if os.path.islink(link_path) or os.path.exists(link_path):
            os.remove(link_path)
        os.symlink(csv_path, link_path)

        while True:
            voltage = get_voltage(ad, ADPI_CH)
            distance = calc_distance(voltage)
            with open(csv_path, "a", newline="") as f:
                writer = csv.writer(f)
                writer.writerow([
                    datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    "{:.9f}".format(distance)
                ])
            sleep(INTERVAL_SEC)

    except (IndexError, ValueError):
        sys.exit(2)
    finally:
        try:
            ctrl_output(ad, ADPI_CH, False)
        except:
            pass
        spi.close()
def calc_distance(v):での換算式について
  • データシートの以下のグラフより、出力電圧と計測距離の逆数は10cmから80cmの範囲で直線的な比例関係があると判断しました。
    image.png

  • 同じくデータシートの表から、80cmのときに0.4V、10cmのときに2.3V(0.4V + 1.9V)と読み取りました。
    image.png

  • 以上より、出力電圧 v と距離 d の関係式は次のように定義しました:d = 22 / (v - 0.13)

※本換算式はセンサの代表特性に基づく近似値であり、実環境では誤差が生じる可能性があります。実運用時には実測値に基づいて補正を行ってください。

web_server.py
from flask import Flask, jsonify, render_template_string
import csv
import os

app = Flask(__name__)

DIR = "/home/mtx/adpi-web-monitor"
CSV_LATEST_PATH = f"{DIR}/sensor_data_latest.csv"
TITLE = "ADPi Monitor"

DATA_NUM = 10 # 表示するデータの件数
REFRESH_INTERVAL = 500  # 画面の更新(ミリ秒)

@app.route("/api/latest")
def api_latest():
    with open(CSV_LATEST_PATH) as f:
        reader = list(csv.DictReader(f))
        rows = reader[-DATA_NUM:]
    return jsonify(rows)

@app.route("/")
def index():
    return render_template_string(f"""
<html>
<head>
    <meta charset="UTF-8">
    <title>{TITLE}</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <h1>{TITLE}</h1>
    <canvas id="chart" width="960" height="540"></canvas>
    <script>
        const REFRESH_INTERVAL = {REFRESH_INTERVAL}; 

        async function fetchData() {{
            const res = await fetch("/api/latest");
            const data = await res.json();

            const labels = data.map(d => d["timestamp"]);
            const distances = data.map(d => parseFloat(d["distance[cm]"]));

            const ctx = document.getElementById('chart').getContext('2d');
            if (window.myChart) {{
                window.myChart.data.labels = labels;
                window.myChart.data.datasets[0].data = distances;
                window.myChart.update();
            }} else {{
                window.myChart = new Chart(ctx, {{
                    type: 'line',
                    data: {{
                        labels: labels,
                        datasets: [{{
                            label: 'Distance (cm)',
                            data: distances,
                            borderWidth: 2
                        }}]
                    }},
                    options: {{
                        responsive: true,
                        scales: {{
                            y: {{
                                min: 0,
                                max: 80,
                                beginAtZero: true
                            }}
                        }}
                    }}
                }});
            }}
        }}

        fetchData();
        setInterval(fetchData, REFRESH_INTERVAL); 
    </script>
</body>
</html>
""")


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000)
Webサーバーのセキュリティ対策について

本記事のWebサーバーはセキュリティ対策を考慮していません。
インターネット越しのアクセスは想定していないため、同一のLAN内での利用に限定してください。
外部公開を行う場合は各種セキュリティ対策の実施をお願いいたします。

次に、Pythonモジュールを実行する仮想環境を構築します。/home/mtx/adpi-web-monitorで次のコマンドを実行してください。

python3 -m venv venv
source venv/bin/activate
sudo apt install python3-dev
pip install flask spidev smbus
cp -r /usr/lib/python3/dist-packages/adpi ~/adpi-web-monitor/venv/lib/python3.11/site-packages/

仮想環境の構築が完了したら下記のコマンドで仮想環境を終了します。

deactivate

ラズパイ起動時に作成したスクリプトを自動実行する

続いて下記の2つのサービスファイル( get_adpi.service と web_server.service )を作成します。ここでは、/home/mtx/adpi-web-monitorに作成しています。

get_adpi.service
[Unit]
After=network.target

[Service]
WorkingDirectory=/home/mtx/adpi-web-monitor
ExecStart=/home/mtx/adpi-web-monitor/venv/bin/python3 get_adpi.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
web_server.service
[Unit]
After=network.target get_adpi.service
Requires=get_adpi.service

[Service]
WorkingDirectory=/home/mtx/adpi-web-monitor
ExecStart=/home/mtx/adpi-web-monitor/venv/bin/python3 web_server.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

下記のコマンドを実行し、ラズパイの起動時に自動実行されるようにします。

sudo cp /home/mtx/adpi-web-monitor/get_adpi.service /etc/systemd/system/
sudo cp /home/mtx/adpi-web-monitor/web_server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable get_adpi.service web_server.service

サービスファイルの設定が完了したらシャットダウンします。

sudo shutdown -h now

起動の確認

メタルケースセットのカバーを閉じます。ADPi ProのCH1のターミナルを一度外し、カバーを閉じてから再度取り付けてください。
今回は、測距モジュールをカバーを留めるねじと共締めしてケースに固定しました。
image.png

ラズパイを起動し、下記のようにIPアドレス(192.168.YY.ZZZ)を確認します。続いてWEBブラウザから確認したIPアドレスを使用し、192.168.YY.ZZZ:8000にアクセスしてください。

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

...

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
    inet 192.168.YY.ZZZ/24 brd 192.168.XX.255 scope global dynamic noprefixroute eth0
       valid_lft 12671sec preferred_lft 12671sec
    inet6 XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX/64 scope global dynamic noprefixroute
       valid_lft 14305sec preferred_lft 14305sec
    inet6 fe80::XXXX:XXXX:XXXX:XXXX/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

...

下記のように計測距離をWEBブラウザに表示し、測距モジュールの前にものを置くと値が変化する様子を確認できました。

PXL_20250529_083555797.gif

注意事項

  • 作成した2つのスクリプト( get_adpi.py と web_server.py )は、仮想環境で実行してください。仮想環境を有効にするにはsource venv/bin/activateを実行します。仮想環境を終了するにはdeactivateを実行します。

  • サービスを使わずに動作確認を行いたい場合は、python3 get_adpi.py &のようにバックグラウンドで get_adpi.py を実行してから python3 web_server.py を実行してください。バックグラウンドで動作中の get_adpi.py を停止するにはpkill -f get_adpi.pyを実行する方法などがあります。

  • get_adpi.py の実行中に強制終了や異常終了した場合などに、ADPi Proからセンサへの電源供給が継続することがあります。電源供給を止めるにはadpictl set output 1 offを実行してください。adpictlコマンドの詳細はこちらを参照してください

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?