はじめに
この記事は、産技品川 Advent Calendar 2025 23日目の記事です!
レーザ距離計ってロマンがありますよね
本記事は、レーザーを使用したモジュールを扱います。
レーザーの扱いには十分気を付けてください。
【安全上のご注意と免責事項】
本記事は、レーザー測距モジュールの通信プロトコル解析および技術的な動作検証を目的とした実験記録です。
実用品としての利用や、他者への危害を加える意図は一切ありません。
実験にあたっての配慮:
・実験は周囲に人がいない安全な環境下で行い、レーザー光を人や動物の目、車両、航空機等に向けて照射することは絶対に避けています。
・本記事は、消費生活用製品安全法(PSC法)に抵触するような、未認可の携帯用レーザー装置の製造・販売を推奨するものではありません。
・本記事の内容を参考に実験を行う際は、レーザーのクラスに応じた適切な安全管理を行い、自己責任にて実施してください。
レーザー距離センサー
作りたいものがあり、
中国のCEサイトであるアリエクにて、レーザ距離センサーを購入しました。

カタログスペックでは精度±2mmとありますが実際のところどうでしょうか?

以下が購入した際のリンクになります。
(アリエクなのでリンク切れになってしまうことがあるかもしれません。
その際は、「レーザ測距モジュール」等で検索をすればヒットすると思います。)
サイト内では、5m, 10m, 20m, 30m, 40m, 50mと選択できる項目があります。と選択できる項目があります。
今回は30メートルのものを購入しました。
価格が
5mのもので約2100円
50mのもので約3500円
と、少し差があります。
ですが、ほかのモジュールを探してみると1万円を超えるものがほとんどです。
3000円で上記のスペックなら十分買う価値はあると考えます。
実験
実際に使用してみたいと思います。
使用したもの
| 器具名 | 品名 | 購入元 |
|---|---|---|
| レーザー距離計 | 不明 | アリエク |
| マイコン | ESP32-C3 SuperMini | アリエク |
| OLED | 128×64ドット OLED | 秋月電子通商 |
| スイッチ | タクトスイッチ(黒色) | 秋月電子通商 |
接続方法
各種部品の接続方法についてです。
例)接続元→接続先
レーザー測距モジュール
黒(GND)→GND
赤(Vcc)→3.3V
緑(RX)→マイコンのTX(ESP-32C3の6番ピン)
黄色(TX)→マイコンのRX(ESP-32C3の7番ピン)
128×64ドット OLED(SSD 1306)
Vcc→3.3V
GND→GND
SDA→マイコンのSDA(ESP-32C3の8番ピン)
SCL→マイコンのSCL(ESP-32C3の9番ピン)
オムロンタクトスイッチ
今回は
一方をマイコンの3番ピンに接続
もう一方をGNDに接続しました。
プログラム
アリエクの販売ページにGithubがあります。
そこにサンプルコードがあります。
サンプルコードを参考にGeminiがコーディングしてくれました。
作成したコード(めちゃ長いので折り畳み)
/**
* ESP32-C3 + レーザー測距儀 (M01系) + SSD1306 OLED + ボタン制御プログラム
* 言語: C++ (Arduino IDE)
* 参考: codigo.txt (Andres Ros氏のコード) をベースにESP32-C3/OLED用に移植
* * * 動作:
* 1. 待機画面でボタンを押す
* 2. M01モジュールに計測コマンド(Quick Measure)を送信
* 3. 13バイトのバイナリデータを受信・解析
* 4. 距離(m)をOLEDに表示
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <HardwareSerial.h>
// --- ピン設定 (ESP32-C3 SuperMini想定) ---
#define LASER_RX_PIN 7 // モジュールのTXへ
#define LASER_TX_PIN 6 // モジュールのRXへ
#define OLED_SDA_PIN 8
#define OLED_SCL_PIN 9
#define BUTTON_PIN 3 // ボタン (GNDとの間に接続)
// --- OLED設定 ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- レーザー測距儀用シリアル ---
HardwareSerial LaserSerial(1);
// --- M01モジュール用コマンド (codigo.txtより) ---
// 読み取りコマンド (Quick Read)
const uint8_t CMD_QUICK[] = {0xAA, 0x00, 0x00, 0x22, 0x00, 0x01, 0x00, 0x00, 0x23};
// --- 状態管理用 ---
enum AppState {
STATE_IDLE, // 待機中
STATE_MEASURING, // 計測中
STATE_RESULT // 結果表示
};
AppState currentState = STATE_IDLE;
float lastMeters = 0.0; // 測定結果(m)
unsigned long measureStartTime = 0; // アニメーション用タイマー
bool measurementDone = false; // 測定完了フラグ
// 受信バッファ用
uint8_t rxBuffer[32];
int rxIndex = 0;
unsigned long lastRxTime = 0;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// OLED初期化
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// レーザー用シリアル初期化
LaserSerial.begin(9600, SERIAL_8N1, LASER_RX_PIN, LASER_TX_PIN);
showIdleScreen();
}
void loop() {
// 1. ボタン監視
if (currentState != STATE_MEASURING) {
if (digitalRead(BUTTON_PIN) == LOW) {
delay(50); // チャタリング防止
if (digitalRead(BUTTON_PIN) == LOW) {
startMeasurement();
while(digitalRead(BUTTON_PIN) == LOW); // 離すのを待つ
}
}
}
// 2. 計測中の処理
if (currentState == STATE_MEASURING) {
processLaserData();
updateProgressBar();
// タイムアウト処理 (2秒経っても来なかったら諦める)
if (millis() - measureStartTime > 2000) {
Serial.println("Timeout!");
lastMeters = -1.0; // エラー値
measurementDone = true;
}
}
// 3. 完了処理
if (measurementDone) {
showResultScreen();
measurementDone = false;
}
}
// --- 計測開始 ---
void startMeasurement() {
Serial.println("Sending Quick Measure Command...");
currentState = STATE_MEASURING;
measureStartTime = millis();
// バッファクリア
while(LaserSerial.available()) LaserSerial.read();
rxIndex = 0;
measurementDone = false;
// コマンド送信
LaserSerial.write(CMD_QUICK, sizeof(CMD_QUICK));
}
// --- データ受信・解析 (codigo.txtのロジックを移植) ---
void processLaserData() {
while (LaserSerial.available()) {
uint8_t c = LaserSerial.read();
// 最初のバイトが0xAAでなければ無視 (同期合わせ)
if (rxIndex == 0 && c != 0xAA) continue;
rxBuffer[rxIndex++] = c;
lastRxTime = millis();
// 13バイト揃ったら解析
if (rxIndex == 13) {
// ヘッダチェック: 0xAA, ... , 0x00, 0x04 (パケット種類によるがcodigo.txt準拠)
// codigo.txt: if(f[0]==0xAA && f[4]==0x00 && f[5]==0x04 && csumOK(f,13))
// ここでは簡易的にチェックサムとヘッダを確認
if (rxBuffer[0] == 0xAA && checkChecksum(rxBuffer, 13)) {
// データ種別チェック (0x22: 成功データ など)
uint8_t func = rxBuffer[3];
if (func == 0x22 || func == 0x20 || func == 0x21) {
// BCDデコードして距離(m)に変換
// データは buf[6], buf[7], buf[8], buf[9] に入っている
uint32_t val = bcdToUint32(&rxBuffer[6]);
lastMeters = val / 1000.0; // mm -> m変換
Serial.print("Distance: ");
Serial.print(lastMeters, 3);
Serial.println(" m");
measurementDone = true;
}
} else {
Serial.println("Checksum Error or Invalid Header");
}
// 次のパケットのためにリセット
rxIndex = 0;
}
}
// タイムアウトによるバッファリセット (断片化したデータの破棄)
if (rxIndex > 0 && millis() - lastRxTime > 100) {
rxIndex = 0;
}
}
// --- ユーティリティ関数 (codigo.txtより移植) ---
// チェックサム確認
bool checkChecksum(const uint8_t* data, int len) {
if (len < 3) return false;
uint32_t sum = 0;
// 先頭(0xAA)を除く、最後(CS)の手前までを足す
for (int i = 1; i < len - 1; i++) {
sum += data[i];
}
return (uint8_t)sum == data[len - 1];
}
// BCD (Binary Coded Decimal) 4バイトを数値に変換
uint32_t bcdToUint32(const uint8_t* b) {
uint32_t v = 0;
for (int i = 0; i < 4; i++) {
// 上位4ビット * 10 + 下位4ビット
v = v * 100 + ((b[i] >> 4) & 0x0F) * 10 + (b[i] & 0x0F);
}
return v;
}
// --- 画面描画関数 ---
void showIdleScreen() {
currentState = STATE_IDLE;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 20);
display.println("PUSH BUTTON");
display.setCursor(35, 35);
display.println("TO SCAN");
display.display();
}
void updateProgressBar() {
int progress = (millis() - measureStartTime) % 1000;
int fillW = (millis() / 5) % (SCREEN_WIDTH - 24);
display.clearDisplay();
display.setTextSize(1);
display.setCursor(30, 20);
display.println("Scanning...");
display.drawRect(10, 40, SCREEN_WIDTH - 20, 10, SSD1306_WHITE);
display.fillRect(12, 42, fillW, 6, SSD1306_WHITE);
display.display();
}
void showResultScreen() {
currentState = STATE_RESULT;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Result:");
display.setTextSize(2);
display.setCursor(10, 25);
if (lastMeters < 0) {
display.println("Error");
} else {
display.print(lastMeters, 3); // 小数点3桁まで
display.setTextSize(1);
display.println(" m");
}
display.setTextSize(1);
display.setCursor(20, 55);
display.println("[BTN] -> Retry");
display.display();
}
レーザーモジュールは、一度の計測で13個(バイト)のパケットを送信します。
6.7.8.9番目のパケットにて、10進数で測定データを送信するため
素の状態(16進数)でデータを受け取ってしまうと、正確なデータが受け取れなくなってしまう場合があります。
実際に測ってみる
15cm定規の上で、1cmずつ動かして測ってみました
定規の端には壁を置いてあります。
ボタンを押してから0.3sほどで測定が終了します。
表示が0.15m→0.14m→0.13m、、、と1cmずつ減っていることが分かると思います。
近づけると測定する時間が短くなりました。
動画にはありませんが、1cmの地点ではボタンを押した瞬間に測定が終了したようなスピード感です。
実際に測ってみる(長距離)
万力を使い金属板を垂直に固定したものでレーザーを反射させました。

レーザー側も同じくらいの高さの椅子に万力で固定しました。
プログラムを書き込めば、PCにつながなくとも測定可能です。
今回はモバイルバッテリーで駆動

OLEDにはなんと74.64mの表示が!
購入したのは30m対応モデルですが、倍以上の距離を計測できました。
おそらく、実験環境が夕方で周囲が暗かったため、スペック以上の性能が発揮されたのだと推測されます。
どれほど正確かは不明ですが、それ以上遠ざかるとErrorになってしまいます。
実際に測ってみる(メジャーを使って実際の距離も測ってみる)
先の実験(定規の上で実験)で±1cmほどの精度で測れることが分かりました。
ですが、10m以上の比較的長距離のレンジではどうでしょうか?
実際に測ってみましょう。
21.277mとの表示があります。
立って測定をしていたためちょっとずれてしまったのかもしれません
それを無視すると、21.15m地点での誤差は約+0.1m(10cm)でした。
測定中、ゴルフ用の距離計を貸していただきました。
貸していただいたゴルフ用レーザー距離計は16mと表示しました。(誤差4m)
ゴルフ用のものは最大測定距離が700mなど、測定可能レンジの長いものが多いです
そのため、5~10mのレンジで測定することは得意でなかった可能性があります。
ゴルフ用のものを分解して使うという記事もありましたが、得意不得意がある場合にこのモジュールは有効であると考えられます。
おわりに
今回の記事で、アリエクで買ったレーザー距離計の精度が(値段に対して)良いと分かりました。
3000円の部品にしてはよくできていると思いました。
同じような距離を測る部品にTofレーザーというものがあります。
こちらも同じくレーザ(不可視光が主)を使用して距離を測ります。
Tofレーザーは測定周期や測定時間に優れますが、遠くまでは測れません。
距離を測る工作物を作りたい!と思ったときやTofでは厳しい、となった場合の代替としてどうでしょうか?
こんなものがあるんだなぁと、少しでも多くの人に便利なものが届けば幸いです。
読んでいただきありがとうございました!




