はじめに
前編では、膨大な配線を1本にまとめる「アドレス指定型LED(WS2812B)」の導入と、ラズパイからの点灯テストを行いました。 今回の後編では、Excelデータ(MasterとOrder)を読み取り、Flaskで立ち上げたサーバーを経由してスマホ操作でLEDを光らせる要素設計のゴールを目指します。
ロードマップ
デジタルピッキングシステムの要素設計ロードマップです。
後編となる本記事はSTEP2~STEP3まで実施します
STEP1:環境構築
- ラズパイとLED(WS2812B)の物理接続
- Pythonライブラリ(rpi_ws281x等)のインストール
- プログラムからLEDを1個だけ光らせる(疎通確認)
STEP 2:要素技術の連携
- ExcelファイルをPythonで読み込む
- 入力したIDに紐づく特定のLEDを光らせる
- NEXT: Order シートを読み込んで「部品名・数量・LED番号」をセットで取得する
STEP 3:UI(操作画面)の実装
- Flask(Webサーバー)を立ち上げる
- タブレットのブラウザに「部品名」と「数量」を大きく表示する
- 「完了」ボタンを押すと次の部品に進むロジックを作る
STEP 2:Excelデータとの連携と自動巡回
Excelファイルの準備
まずは、読み取り用のExcelファイルを準備します。
- ファイル名:parts_list.xlsx
- Sheet名:「Master」と「Order」
- Sheet: Masterの列名:
PartsID、部品名、LED_Index - Sheet: Orderの列名:
指示ID、部品ID、数量
部品構成および結線図
テストコードの実行
正しくExcelファイルからデータを読み取り、対応するLEDを光らせることができるかテストを実施します。 STEP2は、Orderシートに記載した順番にLEDが切り替わる「自動巡回ピッキング」のテストです
権限を持たせて実行

※実行には権限が必要です
ニンジンはラズパイ上でPythonを動かす場合はThonnyで動作させますが、単純にThonnyを立ち上げると動作しません。
「sudo」を付けてThonnyに権限を持たせて立ち上げます。
テストコード:test_led_Excel.py
テストコードの確認はこちらをクリック
import board
import neopixel
import pandas as pd
# LED設定
PIXEL_PIN = board.D18
NUM_PIXELS = 60
pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=0.1, auto_write=False)
def start_picking():
# 1. データの読み込み (CSVファイル名に合わせる)
try:
path = '/home/pi/Desktop/parts_list.xlsx'
df_master = pd.read_excel('parts_list.xlsx', sheet_name='Master', engine='openpyxl')
df_order = pd.read_excel('parts_list.xlsx', sheet_name='Order', engine='openpyxl')
except Exception as e:
print(f"ファイル読み込み失敗: {e}")
return
print("--- ピッキング開始 ---")
# 2. 指示(Order)を1行ずつループ
for index, row in df_order.iterrows():
p_id = str(row['部品ID'])
qty = row['数量']
# 3. MasterからLEDの場所を探す
master_row = df_master[df_master['PartsID'].astype(str) == p_id]
if not master_row.empty:
led_idx = int(master_row['LED_Index'].values[0])
p_name = master_row['部品名'].values[0]
# 4. LED点灯
pixels.fill((0, 0, 0)) # 一旦全部消す
pixels[led_idx] = (0, 255, 0) # 緑色に点灯
pixels.show()
# 5. コンソールに指示表示
print(f"\n【指示 {index + 1}/{len(df_order)}】")
print(f"場所: {led_idx}番の棚")
print(f"部品: {p_name} (ID: {p_id})")
print(f"数量: {qty} 個")
# 6. 作業完了の待機
input("取り出したら [Enter] を押して次へ...")
else:
print(f"警告: ID {p_id} がマスタに登録されていません。")
# 7. 全件終了
pixels.fill((0, 0, 0))
pixels.show()
print("\n--- 全てのピッキングが完了しました!お疲れ様でした。 ---")
if __name__ == "__main__":
start_picking()
test_led_Excel.pyはこちらからダウンロードできます
重要!
※Excelファイル:parts_list.xlsxはラズパイのOSのデスクトップ上に置いて下さい
テスト結果:test_led_Excel.py
STEP 3:Flaskを使ったWeb UIの実装
ラズパイ上でイメージ通りの動作が確認できたので、次はこの動きをネットワーク上で再現します。
Pythonの「Flask(フラスク)」というライブラリを使って、ラズパイ自身をWebサーバーにしてしまいます。
こうすることで、タブレットに専用アプリを入れる必要がなく、ブラウザ(ChromeやSafari)からラズパイにアクセスするだけで操作画面が表示されます。
Excelファイルの準備
STEP2と同じExcelファイルを使用します。
部品構成および結線図
部品構成もSTEP1、STEP2と同じです。
テストコードの実行
いよいよ、ラズパイをサーバーとしてWebUIで操作してみます。
権限を持たせて実行

※実行には権限が必要です
ニンジンはラズパイ上でPythonを動かす場合はThonnyで動作させますが、単純にThonnyを立ち上げると動作しません。
「sudo」を付けてThonnyに権限を持たせて立ち上げます。
テストコード:web_picking.py
テストコードの確認はこちらをクリック
import board
import neopixel
import pandas as pd
from flask import Flask, render_template_string, redirect, url_for
app = Flask(__name__)
# --- 設定エリア ---
PIXEL_PIN = board.D18
NUM_PIXELS = 60
EXCEL_FILE = '/home/pi/Desktop/parts_list.xlsx'
# LED初期化
pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=0.1, auto_write=False)
# データ読み込み
df_master = pd.read_excel(EXCEL_FILE, sheet_name='Master', engine='openpyxl')
df_order = pd.read_excel(EXCEL_FILE, sheet_name='Order', engine='openpyxl')
# 現在のピッキング進行状況を記憶する変数
current_index = 0
# --- HTMLデザイン (タブレット向けUI) ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Picking System</title>
<style>
body { font-family: sans-serif; text-align: center; background-color: #eef2f5; padding: 20px; margin: 0; }
.logo { width: 40px; height: 40px; margin-right: 10px; }
.card { background: white; border-radius: 15px; padding: 40px 20px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); max-width: 600px; margin: 0 auto; }
.progress { color: #666; font-size: 1.2em; margin-bottom: 10px; }
.part-id { color: #888; font-size: 1.5em; }
.part-name { font-size: 3em; font-weight: bold; margin: 10px 0; color: #333; }
.qty-box { background-color: #fff4e5; border-radius: 10px; padding: 20px; margin: 20px 0; border: 2px solid #ff8c00; }
.qty-label { font-size: 1.5em; color: #ff8c00; }
.qty { font-size: 5em; font-weight: bold; color: #d9534f; margin: 0; }
.btn { display: block; width: 100%; padding: 5px; font-size: 2.5em; background-color: #ff8c00; color: white; border: none; border-radius: 15px; text-decoration: none; font-weight: bold; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.2); box-sizing' border-box }
.btn:active { transform: translateY(4px); box-shadow: none; }
.btn-reset { background-color: #6c757d; font-size: 1.5em; padding: 15px; margin-top: 30px; }
.finished { font-size: 2.5em; color: #5cb85c; font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h2 style="color:#ff8c00; margin:0;">Digital Picking</h2>
</div>
<div class="card">
{% if finished %}
<p class="finished">🎉 全てのピッキングが完了しました!</p>
<a href="/reset" class="btn btn-reset">最初からやり直す</a>
{% else %}
<div class="progress">指示 {{ current }} / {{ total }}</div>
<div class="part-id">ID: {{ p_id }}</div>
<div class="part-name">{{ p_name }}</div>
<div class="qty-box">
<div class="qty-label">取り出す数量</div>
<div class="qty">{{ qty }} <span style="font-size: 0.4em; color: #333;">個</span></div>
</div>
<a href="/next" class="btn">完了 (次へ)</a>
{% endif %}
</div>
</body>
</html>
"""
def update_led():
"""現在のインデックスに合わせてLEDを点灯する関数"""
global current_index
pixels.fill((0, 0, 0)) # 一旦全部消す
if current_index < len(df_order):
row = df_order.iloc[current_index]
p_id = str(row['部品ID'])
master_row = df_master[df_master['PartsID'].astype(str) == p_id]
if not master_row.empty:
led_idx = int(master_row['LED_Index'].values[0])
pixels[led_idx] = (0, 255, 0) # 緑色に点灯
pixels.show()
# --- Webサーバーのルーティング設定 ---
@app.route('/')
def index():
global current_index
# 全件完了している場合
if current_index >= len(df_order):
pixels.fill((0, 0, 0))
pixels.show()
return render_template_string(HTML_TEMPLATE, finished=True)
# 現在の指示データを取得
row = df_order.iloc[current_index]
p_id = str(row['部品ID'])
qty = row['数量']
master_row = df_master[df_master['PartsID'].astype(str) == p_id]
p_name = master_row['部品名'].values[0] if not master_row.empty else "不明な部品"
# 画面描画と同時にLEDを更新
update_led()
return render_template_string(
HTML_TEMPLATE,
finished=False,
p_name=p_name,
p_id=p_id,
qty=qty,
current=current_index+1,
total=len(df_order)
)
@app.route('/next')
def next_item():
global current_index
current_index += 1
return redirect(url_for('index'))
@app.route('/reset')
def reset():
global current_index
current_index = 0
return redirect(url_for('index'))
if __name__ == "__main__":
# 起動時にLEDをリセット
pixels.fill((0, 0, 0))
pixels.show()
# 外部(タブレット)からのアクセスを許可してポート5000で起動
app.run(host='0.0.0.0', port=5000)
web_piking.pyはこちらからダウンロードできます。
テストのやり方
重要!
※Excelファイル:parts_list.xlsxはラズパイのOSのデスクトップ上に置いて下さい
重要!!
実施する際は「ラズパイ」+「PC or スマホ」は同一ネットワーク下にてテストを実施して下さい。
WebUIにつながらない時は
つながらない時は?
使用しているネットワークが、デバイス間通信がパブリック設定(禁止)になっていないか確認してみて下さい。
テスト結果:web_picking.py
動画のように「完了(次へ)」をクリックする事に、次のアイテムが表示され、対応するLEDが点灯すれば成功です
まとめと今後の課題
これでデジタルピッキングの心臓部分の要素設計が完成しました。
ただし、実際の現場で数千~数万点という数の暴力に対応するためには、以下のような課題があります。
- 中継基板の導入:Arduino Unoなどを中継基板として活用する。
- データベース化:Excelデータでは動作が重くなるため、部品マスターをSQLで持たせる。
- BOM連携:構成表(BOM)の読み込みを端末側で行う。
- 等など
課題はまだまだありますが、根幹の設計はこんな感じでしょうか。
ぜひ皆さんも、ご自身の環境に合わせてピッキングシステムを構築してみてください!



















コメント