XBeeモジュールの使い方(802.15.4での温度センサネットワーク:スクロールグラフ)


 前回はファームウェアを802.15.4に設定しアナログ温度センサMCP9701(Microchip社製)を3個のエンドデバイスXBeeに接続し、Cyclic Sleep Modeを用いて60秒間隔でコーディネータXBeeに温度結果を送信しました。今回はコーディネータXBeeで受信した3個のエンドデバイスXBee からの温度データやRSSIを読み取り、Processing を用いてリアルタイムでデータを表示するスクロールグラフをご紹介します。

 Processingについては5月24日の「XBeeモジュールの使い方(アナログ温度センサの温度データ可視化)」の記事を参考にしてください。今回はこのときの記事で記載した設定条件でスクロールグラフを作成しました。

 以下のコードで3個のエンドデバイスの温度データをスクロールグラフで表示します。以下のコードをProcessingで実行する前に、ローカルXBeeとのシリアル通信のCOMポート番号とボーレートを48行目に入力してください。

xbee = new Serial(this, “COM14”, 9600);

 また28行目では温度データをcsvファイルに保管するインターバルを設定することができます。デフォルトでは1時間に設定しています。

int interval = 3600000; // 1時間 ミリ秒で指定 10分=600,000ミリ秒

import processing.serial.*;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.ArrayList;

Serial xbee;                    // input serial port from the Xbee Radio
int[] packet = new int[14];     // with 5 samples, the Xbee packet is 24bytes long
int byteCounter;                // keeps track of where you are in the packet
int fontSize = 24;              // size of the text on the screen
int dc = 0;                     // データ数
int Addr = 0;
int addrRead = 0;
int RSSI = 0;
byte thisSample1;
float minTemp = 10;
float maxTemp = 40;
float tempkizami = 0;
String address;                 // sender's address
float Signal = 0;               // average of the sensor data
float Signalv = 0;
int firstRectPos = 25;          // horizontal pos of the first graph bar

int AddrNum = 3; // アドレスの数
ArrayList<Float>[] signals = new ArrayList[AddrNum]; // 各アドレスごとの温度データを保存するリスト
ArrayList<Float>[] timeSeriesData = new ArrayList[AddrNum]; // 各アドレスごとの時系列データを保存するリスト
ArrayList<String>[] timeLabels = new ArrayList[AddrNum]; // 各アドレスごとの時間ラベルを保存するリスト
int interval = 3600000; // 1時間 ミリ秒で指定 10分=600,000ミリ秒
int lastSaveTime = 0; // 最後にデータを保存した時間

float[] temperatures = new float[AddrNum];
float[] rssiValues = new float[AddrNum];
PFont font;

void setup() {
  size(1500, 800, P3D); // ウィンドウサイズを大きくしてNumSensor個のメータを表示
  surface.setResizable(true);
  frameRate(1);  // 1秒間隔で更新
  font = createFont("MS P明朝", 20);
  textFont(font);
  
  // create a font with the second font available to the system:
  PFont myFont = createFont(PFont.list()[5], fontSize);
  textFont(myFont);
  // get a list of the serial ports:
  println(Serial.list());   
  // open the serial port attached to your Xbee radio:
  xbee = new Serial(this, "COM14", 9600); 
  
  // 初期化
  for (int i = 0; i < AddrNum; i++) {
    signals[i] = new ArrayList<Float>();
    timeSeriesData[i] = new ArrayList<Float>();
    timeLabels[i] = new ArrayList<String>();
  }
  lastSaveTime = millis();
}

void draw() {
  background(35, 59, 108);
  
  // set the background:
  fill(255, 255, 0);
  
  // 各メータの位置とサイズ
  float meterSize = 200;
  float[] meterX = new float[10];
  for (int i = 0; i < AddrNum; i++) {
   meterX[i] = (1 + i) * width / (AddrNum + 1);
  }
  float meterY = height / 3;

  for (int i = 0; i < AddrNum; i++) {
    fill(245, 220, 189);
    rect(meterX[i] - 170, meterY - 140, 340, 320, 20);
    drawMeter(meterX[i], meterY, meterSize, temperatures[i], rssiValues[i], i + 1);
  }

  // 時系列データのグラフ描画
  drawTimeSeriesGraph();

  // 指定の時間が経過したらデータを保存
  if (millis() - lastSaveTime > interval) {
    saveData();
    lastSaveTime = millis();
  }
}

void drawBar(int rectHeight, int rectWidth, int rectNum ) {
  if (rectHeight > 0 ) {
    stroke(255, 127, 0);
    fill(123, 255, 0);
  } 
}

void serialEvent(Serial xbee) {
  int thisByte = xbee.read();
  if (thisByte == 0x7E) {   // start byte
    if (packet[5] > 0) {
      parseAddr(packet);
    }  
    if (packet[6] > 0) {
      parseRSSI(packet);
    }  
    // parse the previous packet if there's data:
    if (packet[11] > 0) {
      parseData(packet);
    }
    // reset the byte counter:
    byteCounter = 0;        
  }
  // put the current byte into the packet at the current position:
  packet[byteCounter] = thisByte;
  //  increment the byte counter:
  byteCounter++;
}

void parseRSSI(int[] thisPacket) {
  int RSSI_StartByte = 6;
  RSSI = -thisPacket[RSSI_StartByte];
  rssiValues[Addr - 1] = RSSI;
  println("RSSI:" + RSSI);
}

void parseAddr(int[] thisPacket) {
  int addrRead = 5;
  Addr = thisPacket[addrRead];
  println("Addr:" + Addr);
}

void parseData(int[] thisPacket) {
  int adcStart = 11;              // ADC reading starts at

  // ADC 10-bit value = high byte * 256 + low byte:
  int thisSample = (thisPacket[adcStart] * 256) + thisPacket[1 + adcStart];
  println(thisPacket[adcStart]);
  println(thisPacket[1 + adcStart]);
  //Signalv = float(thisSample) / 310;    //  for LM90
  Signalv = float(thisSample) / 838.5;  // for MCP8701
  println(Signalv);
  //Signal = sqrt(2196200+(1.8639-Signalv)/(3.88E-6)) - 1481.96;  // for LM90
  Signal = (Signalv*1000 - 400) / 19.5; // for MCP9701
  println("Signal:" + Signal);
  
  // アドレスに応じてデータを保存
  if (Addr >= 1 && Addr <= AddrNum) {
    signals[Addr - 1].add(Signal);
    temperatures[Addr - 1] = Signal;
    timeSeriesData[Addr - 1].add(Signal); // 時系列データに追加
    timeLabels[Addr - 1].add(new SimpleDateFormat("HH:mm:ss").format(new Date())); // 時間ラベルを追加
  }
}

void saveData() {
  String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
  String fileName = "C:/Users/kurita/Desktop/TempData/TempData_" + timestamp + ".csv";
  PrintWriter output = createWriter(fileName);

  // ヘッダー行の作成
  String header = "Time";
  for (int i = 0; i < AddrNum; i++) {
    header += ",Addr " + (i + 1);
  }
  output.println(header);

  // データ行の作成
  int maxRows = 0;
  for (int i = 0; i < AddrNum; i++) {
    maxRows = max(maxRows, signals[i].size());
  }
  
  for (int row = 0; row < maxRows; row++) {
    String line = "";
    for (int col = 0; col < AddrNum; col++) {
      if (col > 0) line += ",";
      if (row < timeLabels[col].size()) {
        line += timeLabels[col].get(row) + ",";
      } else {
        line += ",";
      }
      if (row < signals[col].size()) {
        line += signals[col].get(row);
      }
    }
    output.println(line);
  }
  
  output.flush();
  output.close();
  
  println("Data saved to " + fileName);
  
  // データのリセット
  for (int i = 0; i < AddrNum; i++) {
    signals[i].clear();
    timeSeriesData[i].clear();
    timeLabels[i].clear();
  }
}

void drawMeter(float meterX, float meterY, float meterSize, float temperature, float rssi, int sensorID) {
  pushStyle(); 
  // 温度スケール
  float minTemp = 0;
  float maxTemp = 50;

  // 温度を表示する角度の計算 (左に90度回転)
  float startAngle = radians(-225);
  float endAngle = radians(45);
  float angle = map(temperature, minTemp, maxTemp, startAngle, endAngle);

  // メータのバックグラウンド
  stroke(200);
  strokeWeight(10);
  //fill(200, 200, 200);
  noFill();
  arc(meterX, meterY, meterSize, meterSize, startAngle, endAngle);

  // メータの目盛り
  int tickCount = 10;
  for (int i = 0; i <= tickCount; i++) {
    float tickAngle = map(i, 0, tickCount, startAngle, endAngle);
    float x1 = meterX + cos(tickAngle) * meterSize / 2;
    float y1 = meterY + sin(tickAngle) * meterSize / 2;
    float x2 = meterX + cos(tickAngle) * meterSize / 1.9;
    float y2 = meterY + sin(tickAngle) * meterSize / 1.9;
    line(x1, y1, x2, y2);

    // メモリ値の表示
    fill(70);
    textSize(19);
    textAlign(CENTER, CENTER);
    float labelX = meterX + cos(tickAngle) * meterSize / 1.7;
    float labelY = meterY + sin(tickAngle) * meterSize / 1.7;
    text(int(i), labelX, labelY);
  }

  // 温度を表示する部分
  stroke(255, 0, 0);  // 赤色
  strokeWeight(10);
  fill(245, 220, 189);
  arc(meterX, meterY, meterSize, meterSize, startAngle, angle);

  // 指示針の描画(黒色の三角矢印)
  fill(255, 215, 0);
  stroke(0);
  strokeWeight(1);
  float needleLength = meterSize / 2.0;
  float arrowSize = 10;
  float x1 = meterX + cos(angle) * needleLength;
  float y1 = meterY + sin(angle) * needleLength;
  float x2 = meterX + cos(angle + radians(90)) * arrowSize;
  float y2 = meterY + sin(angle + radians(90)) * arrowSize;
  float x3 = meterX + cos(angle - radians(90)) * arrowSize;
  float y3 = meterY + sin(angle - radians(90)) * arrowSize;
  float x4 = meterX + cos(angle) * (needleLength - arrowSize);
  float y4 = meterY + sin(angle) * (needleLength - arrowSize);

  beginShape();
  vertex(x1, y1);
  vertex(x2, y2);
  vertex(x3, y3);
  vertex(x4, y4);
  endShape(CLOSE);

  fill(0, 0, 255);  // 針の軸を青に塗りつぶす
  ellipse(meterX, meterY, 20, 20);

  // 現在の温度をテキストで表示
  fill(0);
  textSize(24);
  textAlign(CENTER, CENTER);
  text("Sensor " + sensorID + ":  " + nf(temperature, 1, 2) + "[°C]", meterX, meterY + meterSize / 2 + 50);

  // RSSIのカラーグラデーションバーを表示
  float rssiBarWidth = 30;
  float rssiBarHeight = meterSize;
  float rssiBarX = meterX + meterSize / 1.5;
  float rssiBarY = meterY + meterSize / 2 - rssiBarHeight;
  float rssiLength = map(rssi, -255, 0, 0, rssiBarHeight);

  // RSSIのバックグラウンドを白で表示
  fill(255);
  noStroke();
  rect(rssiBarX, rssiBarY, rssiBarWidth, rssiBarHeight);

  // RSSIのカラーグラデーションバー
  for (int i = 0; i < rssiLength; i++) {
    float inter = map(i, 0, rssiBarHeight, 1.0, 0.0);
    int c = lerpColor(color(255, 0, 0), color(255, 255, 0), inter); // 赤から黄色のグラデーション
    stroke(c);
    line(rssiBarX, rssiBarY + rssiBarHeight - i, rssiBarX + rssiBarWidth, rssiBarY + rssiBarHeight - i);
  }

  // 信号が最大のときにバーの周囲を黒線で囲む
  if (rssi >= -50) {
    stroke(0);
    noFill();
    rect(rssiBarX, rssiBarY, rssiBarWidth, rssiBarHeight);
  }

  // RSSIのラベルを表示
  fill(0);
  textSize(20);
  textAlign(CENTER, CENTER);
  text("RSSI", rssiBarX - 3 + rssiBarWidth / 2, rssiBarY - 20);

  // 現在のRSSIを数値で表示
  fill(0);
  textSize(20);
  textAlign(CENTER, CENTER);
  text(nf(rssi, 2, 0) + "dBm", rssiBarX - 13 + rssiBarWidth / 2, rssiBarY + rssiBarHeight + 20);
  popStyle(); 
}

void drawTimeSeriesGraph() {
  float graphX = 200;
  float graphY = height * 1.13 / 2;
  float graphWidth = width - 400;
  float graphHeight = 200;
  
  // グラフのバックグラウンド
  fill(245, 220, 189); // 肌色
  noStroke();
  rect(graphX, graphY, graphWidth, graphHeight);
  
  // グラフの枠を描画
  stroke(255);
  noFill();
  rect(graphX, graphY, graphWidth, graphHeight);

  // 縦軸の目盛りとラベルを表示
  fill(255);
  textSize(26);
  textAlign(CENTER);
  tempkizami = ( maxTemp - minTemp)/ 5;
  for (int i = 0; i <= tempkizami; i++) {
    float y = map(i, 0, tempkizami, graphY + graphHeight, graphY);
    stroke(0, 0, 0);
    strokeWeight(1);
    line(graphX, y, graphX + graphWidth, y); // 目盛線を描画
    stroke(255, 255, 255);
    line(graphX - tempkizami, y, graphX, y);
    text(nf(minTemp + i * 5, 1, 0), graphX - 25, y + 8); // 10°Cから40°Cまでの目盛り
  }
  // グラフのタイトルを表示
  textSize(36);
  textAlign(CENTER);
  text("Temperature sensor network monitor", graphX + graphWidth / 2, 40);
  
  // センサーの色のラベルを表示
  textSize(20);
  textAlign(CENTER);
  fill(255, 0, 0);
  text("Sensor 1", graphX + graphWidth / 1.3, graphY + graphHeight + 70);
  fill(0, 255, 0);
  text("Sensor 2", graphX + graphWidth / 1.18, graphY + graphHeight + 70);
  fill(255, 0, 255);
  text("Sensor 3", graphX + graphWidth / 1.08, graphY + graphHeight + 70);

  // グラフのラベルを表示
  textSize(36);
  textAlign(CENTER);
  fill(255, 255, 255);
  text("Time", graphX + graphWidth / 2, graphY + graphHeight + 90);
  
  // 縦軸のラベルを縦書きで表示
  textSize(36);
  textAlign(CENTER);
  pushMatrix();
  translate(graphX - 80, graphY + graphHeight / 2);
  rotate(-PI / 2);
  text("Temperature [°C]", 0, 0);
  popMatrix();

  // 時間スケールを表示
  for (int i = 0; i < AddrNum; i++) {
    textSize(26);
    textAlign(CENTER);
    int numLabels = min(timeLabels[i].size(), 5); // 時間ラベルを5個以下に制限
    for (int j = 0; j < numLabels; j++) {
      int index = int(map(j, 0, numLabels - 1, 0, timeLabels[i].size() - 1));
      float x = map(index, 0, timeLabels[i].size() - 1, graphX, graphX + graphWidth);
      float y = graphY + graphHeight + 40;
      // 前の時間ラベルをバックグラウンドカラーで塗りつぶす
      fill(35, 59, 108); // バックグラウンドカラーと同じ色
      noStroke();
      rect(x - 50, y - 20, 100, 20);
      // 新たな時間ラベルを表示
      fill(255); // 白色で描画
      text(timeLabels[i].get(index), x, y);
      // 短い目盛線を描画
      stroke(255); // 白色の目盛線
      strokeWeight(1); // 細い線
      line(x, graphY + graphHeight, x, graphY + graphHeight + 10); // 短い目盛線
    }
  }

  // 各アドレスごとの温度データをプロット
  color[] colors = {color(255, 0, 0), color(0, 255, 0), color(255, 0, 255)};
  for (int i = 0; i < AddrNum; i++) {
    stroke(colors[i]);
    strokeWeight(3);
    noFill();
    beginShape();
    for (int j = 0; j < timeSeriesData[i].size(); j++) {
      float x = map(j, 0, timeSeriesData[i].size() - 1, graphX, graphX + graphWidth);
      float y = map(timeSeriesData[i].get(j), 10, 40, graphY + graphHeight, graphY);
      vertex(x, y);
    }
    endShape();
  }
}