冬の特別実験 12月開催開始予定 →
ROBO SCIENCE

まわりの形が画面に出る ―
LiDAR の360度を見る

2026.11.03
ER(Expert-rescue)
板を丸めた輪のまん中に置かれた LiDAR。上面に向きを示す矢印のシールが貼ってあり、4本のケーブルが手前へのびている
板を丸めた輪のまん中に置いて、1周ぶんの点が本当に円になるかを確かめる。

前の記事では、正面の1方向だけ距離を読みました。今回は360方向を全部受け取って、上から見た図にします。そのうえで、形のわかっている物の中に入れて、点が本当に壁のとおりに並ぶかを確かめます。

この記事は、正面までの距離を読む ― LiDAR をつなぐ の続きです。配線と、正面1方向の読み方はあちらにあります。パソコン側は Processing を使うので、Processing のはじめ方 も先に見ておいてください。

1周ぶんを、1行で送る

T-mini Plus は1秒に4000回測り、6回転します。1周ぶんは約667点です。

この667点を、1度ずつ360個の箱に入れます。1つの箱に入るのは平均1.9点。だいたいの箱が埋まります。

1周そろったら、箱の中身を1行で送ります。

D,150,150,151,152,…,150,6.0
場所中身
D1周ぶんの始まりの印
2番目〜361番目0度(正面)から右回りに1度ずつの距離[mm]。0 = その方向は測れなかった
最後回転数[Hz]

1行は約2000文字、1秒に6行で 12KB ほどです。USB には余裕があります。

プログラム ― Pico 側

そのまま書き込めます。受信の部分は前の記事と同じで、変えたのは2か所だけです。

変えたところしていること
usePoint()正面の1点だけ覚えるのをやめ、全方向を箱にしまう。同じ箱に2点入ったら、あとの点で上書きする
loop() の最後1周そろったら、360個を1行で送る。送ったら scanReady を下ろす
// =====================================================================
//  T-mini Plus の360度ぶんの距離を、1周ごとに1行で送るサンプル
//
//  1周そろうたびに、USB へ次の1行を出す。
//    D,距離0,距離1,…,距離359,回転数
//  距離はロボット基準の 0度(正面)から右回りに1度ずつ、単位は mm。
//  測れなかった方向は 0。
//
//  受け手は Processing の LidarView。
//  Arduino IDE のシリアルモニタで見てもよい(115200bps)。
//
//  ボード : Raspberry Pi Pico(Arduino IDE / earlephilhower コア)
//  配線   : LiDAR VCC → 5V(1A以上)   LiDAR Tx → GP1(2番)
//           LiDAR GND → GND            LiDAR Rx → GP0(1番)
//
//  受信の部分(handleByte / handlePacket)は FrontSample と同じ。
//  変えたのは、点のしまい方(work[360])と、送り方だけ。
// =====================================================================
#include <Arduino.h>

// ------------------------------------------------------------ 設定
const int PIN_LIDAR_TX = 0;  // GP0 → LiDAR の Rx
const int PIN_LIDAR_RX = 1;  // GP1 ← LiDAR の Tx

// LiDAR の0度がロボット正面から何度ずれているか(時計回りが正)
const float MOUNT_OFFSET_DEG = 0.0f;

const uint16_t MIN_MM = 50;  // データシートの最短測距。これより近い値は捨てる

// ------------------------------------------------------------ 結果
uint16_t dist[360];  // 直前の1周。1度ずつ。0 = その方向は測れなかった
uint16_t work[360];  // いま集めている途中の1周
bool scanReady = false;
float scanHz = 0;
uint32_t goodPackets = 0;
uint32_t badPackets = 0;

// ------------------------------------------------------------ 受信用
uint8_t pkt[10 + 255 * 3];  // ヘッダ10バイト + 1点3バイト × 最大255点
int pktLen = 0;
int pktTotal = 0;

// 1周の区切り: 集め終わった1周を結果に移し、次の1周ぶんを空にする
void newRevolution() {
  memcpy(dist, work, sizeof(dist));
  memset(work, 0, sizeof(work));
  scanReady = true;
}

// 1点ぶん: 近い1度の箱にしまう
void usePoint(float angleDeg, uint16_t mm) {
  if (mm < MIN_MM) return;
  float robotDeg = angleDeg - MOUNT_OFFSET_DEG;
  int i = (int)floorf(robotDeg + 0.5f) % 360;
  if (i < 0) i += 360;
  work[i] = mm;  // 同じ箱に2点入ったら、あとの点で上書きする
}

// =============================================================
//  パケット1つぶんを読む
//  AA 55 | CT | LSN | FSA(2) | LSA(2) | CS(2) | 点(強度1+距離2) × LSN
// =============================================================
void handlePacket() {
  uint8_t ct = pkt[2];
  uint8_t lsn = pkt[3];
  uint16_t fsa = pkt[4] | (pkt[5] << 8);
  uint16_t lsa = pkt[6] | (pkt[7] << 8);
  uint16_t cs = pkt[8] | (pkt[9] << 8);

  // チェックサム
  uint16_t sum = 0x55AA ^ fsa;
  for (int i = 0; i < lsn; i++) {
    uint8_t* s = &pkt[10 + i * 3];
    sum ^= s[0];
    sum ^= s[1] | (s[2] << 8);
  }
  sum ^= (lsn << 8) | ct;
  sum ^= lsa;
  if (sum != cs) {
    badPackets++;
    return;
  }
  goodPackets++;

  // 1周の始まり
  if (ct & 0x01) {
    scanHz = (ct >> 1) / 10.0f;
    newRevolution();
  }

  // 角度
  float a0 = (fsa >> 1) / 64.0f;
  float a1 = (lsa >> 1) / 64.0f;
  float span = a1 - a0;
  if (span < 0) span += 360.0f;

  // 1点ずつ。強度はこのサンプルでは使わない
  for (int i = 0; i < lsn; i++) {
    uint8_t* s = &pkt[10 + i * 3];
    uint16_t mm = (s[1] | (s[2] << 8)) >> 2;
    float angle = (lsn > 1) ? a0 + span * i / (lsn - 1) : a0;
    if (angle >= 360.0f) angle -= 360.0f;
    usePoint(angle, mm);
  }
}

// 1バイト受け取るたびに呼ぶ
void handleByte(uint8_t b) {
  if (pktLen == 0) {
    if (b == 0xAA) pkt[pktLen++] = b;
    return;
  }
  if (pktLen == 1) {
    if (b == 0x55) pkt[pktLen++] = b;
    else if (b != 0xAA) pktLen = 0;
    return;
  }
  pkt[pktLen++] = b;
  if (pktLen == 10) {
    if (pkt[3] == 0) {
      pktLen = 0;
      return;
    }
    pktTotal = 10 + pkt[3] * 3;
  }
  if (pktLen >= 10 && pktLen == pktTotal) {
    handlePacket();
    pktLen = 0;
  }
}

// コマンド送信(A5 + 1バイト)
void sendCommand(uint8_t cmd) {
  uint8_t b[2] = {0xA5, cmd};
  Serial1.write(b, 2);
}

void startScan() {
  sendCommand(0x65);  // 停止
  delay(50);
  while (Serial1.available()) Serial1.read();
  pktLen = 0;
  sendCommand(0x60);  // スキャン開始
}

// =============================================================
//  setup / loop
// =============================================================
uint32_t lastGood = 0;
unsigned long lastDataMs = 0;

void setup() {
  Serial.begin(115200);
  Serial1.setTX(PIN_LIDAR_TX);
  Serial1.setRX(PIN_LIDAR_RX);
  Serial1.setFIFOSize(4096);
  Serial1.begin(230400);
  delay(500);
  startScan();
  lastDataMs = millis();
}

void loop() {
  // 1. 届いたバイトを全部読む
  while (Serial1.available()) handleByte((uint8_t)Serial1.read());

  // 2. 1秒データが来なければ、開始コマンドを送り直す
  if (goodPackets != lastGood) {
    lastGood = goodPackets;
    lastDataMs = millis();
  } else if (millis() - lastDataMs > 1000) {
    startScan();
    lastDataMs = millis();
  }

  // 3. 1周そろったら、360個の距離を1行で送る
  if (scanReady) {
    scanReady = false;
    Serial.print('D');
    for (int i = 0; i < 360; i++) {
      Serial.print(',');
      Serial.print(dist[i]);
    }
    Serial.print(',');
    Serial.println(scanHz, 1);
  }
}

1周の区切りは、LiDAR が教えてくれます。パケットの CT の bit0 が 1 のとき、そこが1周の始まりです。そこで集め終わった work[] を dist[] に移し、work[] を空にします。

送っているあいだも、LiDAR はデータを送り続けています。LiDAR が流してくるのは毎秒13KB ほど。受け取る場所(setFIFOSize(4096))は約0.3秒ぶんあるので、取りこぼしません。

プログラム ― Processing 側

必要なもの
ソフトProcessing 4。入れ方は Processing のはじめ方
Pico上の ScanSample を書き込み、USB でつないでおく
注意Arduino IDE のシリアルモニタは閉じる

送られてきた360個の距離を、上から見た図にします。角度 k 度・距離 mm の点は、画面では次の場所です。

float a = radians(k);
float x =  mm * pxPerMm * sin(a);
float y = -mm * pxPerMm * cos(a);

y に マイナス が付いているのは、画面の y が下向きだからです。0度を上(正面)にするために、符号を逆にしています。

// =====================================================================
//  LidarView ― T-mini Plus の距離を、上から見た図にする(Processing 4)
//
//  Pico に ScanToProcessing を書き込み、USB でつないでから実行する。
//  Arduino IDE のシリアルモニタは閉じておく(同じポートを2つで開けないため)。
//
//  操作
//    マウスのホイール / + -  … 拡大・縮小
//    g                      … 30cm のマス目(メイズのタイル)を出す・消す
//    l                      … となりの点を線でつなぐ・つながない
//    s                      … 今の画面を PNG で保存
// =====================================================================
import processing.serial.*;

Serial port;
String portName = "";
volatile int[] dist = new int[360];  // 最新の1周(mm)。0 は測れなかった方向
volatile float scanHz = 0;
int lineCount = 0;
int lastLineMs = 0;

float pxPerMm = 0.25;      // 1mm を何ピクセルで描くか
boolean showGrid = true;
boolean showLines = true;

void setup() {
  size(900, 900);
  pixelDensity(displayDensity());
  textFont(createFont("Hiragino Sans", 14));

  String[] ports = Serial.list();
  printArray(ports);
  for (String p : ports) {
    if (p.contains("usbmodem")) portName = p;   // Mac で Pico は /dev/cu.usbmodem…
  }
  if (portName.equals("") && ports.length > 0) portName = ports[0];
  if (!portName.equals("")) {
    port = new Serial(this, portName, 115200);
    port.bufferUntil('\n');
  }
}

// 1行届くたびに呼ばれる: D,距離0,…,距離359,回転数
void serialEvent(Serial p) {
  String line = p.readStringUntil('\n');
  if (line == null) return;
  line = trim(line);
  if (!line.startsWith("D,")) return;
  String[] t = split(line, ',');
  if (t.length != 362) return;              // 途中で切れた行は使わない
  int[] d = new int[360];
  for (int i = 0; i < 360; i++) d[i] = int(t[i + 1]);
  dist = d;                                  // 描いている途中で混ざらないよう、まとめて差し替える
  scanHz = float(t[361]);
  lineCount++;
  lastLineMs = millis();
}

void draw() {
  background(250);
  int[] d = dist;

  pushMatrix();
  translate(width / 2, height / 2);

  // 30cm のマス目。ロボットがマスの中央にいるとき、壁の位置と重なる
  if (showGrid) {
    stroke(232, 222, 200);
    strokeWeight(1);
    float half = max(width, height);
    for (int k = -30; k <= 30; k++) {
      float v = (150 + 300 * k) * pxPerMm;
      if (abs(v) > half) continue;
      line(v, -half, v, half);
      line(-half, v, half, v);
    }
  }

  // 50cm ごとの距離の円
  noFill();
  stroke(215);
  for (int r = 500; r <= 12000; r += 500) {
    float rr = r * pxPerMm;
    if (rr > width) break;
    ellipse(0, 0, rr * 2, rr * 2);
    fill(150);
    text((r / 10) + "cm", 4, -rr - 4);
    noFill();
  }

  // 測れた点。角度は正面(上)から右回り
  float px = 0, py = 0;
  boolean prevOk = false;
  for (int i = 0; i <= 360; i++) {
    int k = i % 360;
    int mm = d[k];
    if (mm <= 0) { prevOk = false; continue; }
    float a = radians(k);
    float x = mm * pxPerMm * sin(a);
    float y = -mm * pxPerMm * cos(a);
    // となりの点と 10cm 以内なら、同じ壁とみなして線でつなぐ
    if (showLines && prevOk && abs(mm - d[(k + 359) % 360]) < 100) {
      stroke(25, 113, 194, 160);
      strokeWeight(2);
      line(px, py, x, y);
    }
    stroke(25, 113, 194);
    strokeWeight(4);
    point(x, y);
    px = x; py = y; prevOk = true;
  }

  // ロボット(中心)と正面の向き
  fill(217, 115, 26);
  noStroke();
  ellipse(0, 0, 14, 14);
  stroke(217, 115, 26);
  strokeWeight(3);
  line(0, 0, 0, -30);
  fill(217, 115, 26);
  text("前", -7, -36);
  popMatrix();

  // 左上の文字
  fill(40);
  int y = 24;
  text("ポート: " + (portName.equals("") ? "見つかりません(Pico をつないで、もう一度実行)" : portName), 16, y); y += 22;
  boolean fresh = millis() - lastLineMs < 1000;
  text("回転数: " + nf(scanHz, 0, 1) + " Hz 受け取った周: " + lineCount + (fresh || lineCount == 0 ? "" : "(止まっています)"), 16, y); y += 22;
  text("前 " + near(d, 0) + " 右 " + near(d, 90) + " 後 " + near(d, 180) + " 左 " + near(d, 270), 16, y); y += 22;
  fill(120);
  text("ホイール/+ - 拡大 g マス目 l 線 s 保存", 16, height - 16);
}

// deg の前後 2° のうち、測れた点の中央値(cm で表示)。なければ —
String near(int[] d, int deg) {
  IntList v = new IntList();
  for (int k = -2; k <= 2; k++) {
    int mm = d[(deg + k + 360) % 360];
    if (mm > 0) v.append(mm);
  }
  if (v.size() == 0) return "—";
  v.sort();
  return nf(v.get(v.size() / 2) / 10.0, 0, 1) + "cm";
}

void mouseWheel(MouseEvent e) {
  pxPerMm = constrain(pxPerMm * pow(1.1, -e.getCount()), 0.03, 2.0);
}

void keyPressed() {
  if (key == '+' || key == '=') pxPerMm = min(pxPerMm * 1.2, 2.0);
  if (key == '-') pxPerMm = max(pxPerMm / 1.2, 0.03);
  if (key == 'g') showGrid = !showGrid;
  if (key == 'l') showLines = !showLines;
  if (key == 's') saveFrame("lidar-####.png");
}
キーはたらき
ホイール/+ -拡大・縮小
g30cm のマス目を出す・消す(迷路のタイルに合わせてある)
lとなりの点を線でつなぐ・つながない
sいまの画面を PNG で保存

となりの点を線でつなぐのは、距離の差が 10cm 以内のときだけです。差が大きいところは、別の物を見ている境目です。そこをつながないので、物の切れ目がそのまま切れ目として見えます。

形が出るか ― 円・四角・三角に閉じこめる

点が正しく並んでいるかは、形のわかっている物の中に入れてみるのがいちばん早い確かめ方です。板を丸めた輪、板で囲った四角、板を3枚立てた三角。この3つで試します。

板を丸めた輪の中央に LiDAR を置き、ケーブルでブレッドボード上の Pico につないだ写真 板で四角く囲んだ中央に LiDAR を置き、ケーブルでブレッドボード上の Pico につないだ写真 板を3枚で三角に囲んだ中央に LiDAR を置き、ケーブルでブレッドボード上の Pico につないだ写真
用意したもの。左から 円・四角・三角。板で囲って、まん中に LiDAR を置くだけ。
壁の形が、そのまま点になる 前 円 丸い中 前 四角 四角の中 前 三角 三角の中 うすい帯が本当の壁。青い点が LiDAR の読んだ 360 個の距離
壁の形から計算した「こう出るはず」の絵(大きさは例)。下に、実際の画面を並べる。

以下は、実際に測った画面です。点のあいだは線でつないであります(l キー)。

円に入れて、中心に置いたとき。どの方向も同じ距離になります。中心からずらすと、ずらした向きだけ近くなります。

板を丸めた輪の中。どの向きも 17.3〜17.9cm のあいだ。

確かめの本体は、形ではなく上の行の数字です。円のまん中に置いたのだから、4方向は同じ値になるはずです。画面から9回読みとって、いちばん遠いのが 17.9cm、いちばん近いのが 17.3cm。ひらきは 6mm で、ここにはまん中に置いた精度もふくまれます。角度の割りふりも、距離の読み方も、おかしくはありません。形が円に見えるだけでは、そこまでは言えません。

四角い箱に入れたとき。壁は直線なので、正面がいちばん近く、角がいちばん遠くなります。垂直距離を D、壁の正面からの角度を θ とすると、距離は D ÷ cosθ です。

板で囲った四角の中。前 18.1〜18.5/右 18.0〜18.4/後 17.7〜18.2/左 17.7〜18.1cm。角の外に、点が飛び出している。

4つの角のすぐ外へ、点が何個か飛び出しています。壁と壁のつなぎ目にすき間があれば、その方向だけ光が奥まで抜けます。斜めに当たりすぎて返りが弱いときにも、遠めの値が出ます。角の数点は、信用しない。距離を使うときに1点だけを読まず、まわり何点かの真ん中の値をとるのは、これが理由です。

壁そのものも、数mm波打っています。まっすぐな板でも、点はきれいな直線にはなりません。

三角に入れたとき。遠い角が3つ、近い壁が3つ。四角との違いは、角の数だけです。

三角に立てた板の中。前 12.5/右 12.9/後 13.7/左 17.7cm あたり。左だけ遠いのは、その向きが角だから。

三角では、左だけが 17.7cm と、ほかより 5cm ほど遠くなっています。その方向に壁が無いからではなく、その方向が角だからです。前・右・後ろは壁、左は角。

大事なのは、1周ごとのゆれ(数mm)より、方向どうしの差のほうがずっと大きいことです。前 12.5 と 後 13.7 は 1cm 以上はなれていて、ゆれでは決してまぎれません。だから、数字を見るだけで自分がどちらを向いているかがわかります。

同じ数字を、横軸を角度にして並べると、もっとはっきりします。

同じ数字を、横に並べて見る 100 200 300 0度 → 360度 円 300〜300 mm 0度 → 360度 四角 225〜375 mm 0度 → 360度 三角 173〜345 mm mm 円は平ら。谷の底がいちばん近い壁、山の頂上が角。山と谷の数が壁の数
谷の底が、その壁までの垂直距離。山の頂上が角。

点の間隔は、場所によって変わります。壁の上では正面がいちばん細かく、角へ行くほど粗くなります。間隔は 1 ÷ cos²θ に比例するので、四角の角(45度)では、正面の2倍の粗さです。

見るところこうなっていれば正しい
形入れた物の形が、そのまま出る
大きさ画面に出た形の差し渡しが、物差しで測った値と合う(g を押すと 30cm のマス目が出る)
向きロボットの正面に手をかざすと、画面の上に点が近づく
回転数6Hz 前後。「止まっています」と出ない
欠け黒い物・ガラス・5cm より近い物の方向は、点が出ない

向きがずれていたら、MOUNT_OFFSET_DEG で直します。正面に手をかざして、点がどの角度に出るかを読み、その値を入れます。以後はロボットの正面が 0度になります。

T-mini Plus LiDAR の上面に、向きを示す矢印のシールが貼ってある写真
上に矢印のシールを貼っておく。0度がどこかを、毎回さがさずに済む。

センサーは黒くて丸く、見た目では向きがわかりません。上に矢印を1枚貼っておくと、ロボットに載せるときも、外して机で試すときも、同じ向きに置けます。MOUNT_OFFSET_DEG を一度決めたら、その値が使いつづけられるということです。

動かないときに見るところ ― 症状から原因を引く

症状原因対処
Processing に何も出ないシリアルモニタが開いたままArduino IDE のシリアルモニタを閉じる
「ポート: 見つかりません」名前に usbmodem が入っていない起動時に出るポート一覧を見て、portName に直接書く
「止まっています」と出るPico が送っていないUSB を挿し直す。Pico 側の書き込みを確かめる
点は出るが、形がゆがむLiDAR が斜めに付いている水平に付け直す。壁より低い位置に付ける
形は出るが、向きが合わない取り付けの向きがずれているMOUNT_OFFSET_DEG を入れる
画面の外に出てしまう倍率が合っていないホイールか - で縮める
一部の方向だけ点が出ない黒い物・ガラス・5cm より近い白い紙を置くと出る
形が二重に見える1周ぶんが途中で切れているUSB ケーブルを替える。362個でない行は、Processing 側で捨てている

次は、壁との距離を一定に保ちながら走らせます。ここで読めるようになった距離を、そのままモーターの速さに変える話です。

ロボサイエンスについて

ロボカップジュニア2027 に向けて、レスキュー(災害救助)とサッカーの2種目に取り組みます。プログラミングと電子工作の経験は問いません。

ロボサイエンスのコース紹介 →
これまでの大会成績 →
体験授業・お問い合わせ →

← ロボサイエンスの記録一覧へ戻る