M5Stackペンプロッタをストロークフォント対応にしてみた

前回はM5Stackでペンプロッタを作ったのでストロークフォントで文字の描画に対応してみました

M5Stackでペンプロッタを作ってみた - Qiita
https://qiita.com/coppercele/items/8e35785da1dc717a0970

##ストロークフォントを導入する##

こちらのサイトを参考にさせてもらいました

ストロークフォントを使って PlotClock で文字を描く – しかるのち
https://shikarunochi.matrix.jp/?p=4276


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/3fd0a35f-51ff-832c-443f-a9509353b1d2.png)

バイナリデータの解説があったのでこれを眺めてたら上位3bitで命令の種類を、
下位5bitで数値データを表してるんじゃね？と見当がついたので
Google spreadsheetで関数をこねこねしてみたらだいたいそんな感じでした
(0x20が終端だったりデータが飛んでたり例外も存在するのでそこは処理する)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/e21fe707-22c7-4043-6567-d574e06d9f5f.png)

まず参考にさせてもらうソースを改変してまるっと使わせてもらいます
※記事を書いた人はポインタが分かりません

```cpp

// 戻り値で文字のバイト数を返すように変更
int efontUFT8toUTF16(uint16_t *pUTF16, char *pUTF8) {
  // 1Byte Code
  if (pUTF8[0] < 0x80) {
    // < 0x80 : ASCII Code
    *pUTF16 = pUTF8[0];
    return 1;
  }

  // 2Byte Code
  if (pUTF8[0] < 0xE0) {
    *pUTF16 = ((pUTF8[0] & 0x1F) << 6) + (pUTF8[1] & 0x3F);
    return 2;
  }

  // 3Byte Code
  if (pUTF8[0] < 0xF0) {
    *pUTF16 = ((pUTF8[0] & 0x0F) << 12) + ((pUTF8[1] & 0x3F) << 6) +
              (pUTF8[2] & 0x3F);
    return 3;
  } else {
    // 4byte Code
    *pUTF16 = 0;
    return 4;
  }
}

void loop() {
  M5.update();
  // Aボタンを押すと描画
  if (M5.BtnA.wasReleased()) {
    std::string plotText = "あ";
    Serial.printf("plotText:%s\n", plotText.c_str());
    Serial.printf("plotText.length():%d\n", plotText.length());

    for (int j = 0; j < plotText.length();) {
      uint16_t strUTF16;
      // fontIndex[]のindexを取得して文字のバイト数を取得する
      int textBytes =
          efontUFT8toUTF16(&strUTF16, (char *)(plotText.substr(j).c_str()));
      Serial.printf("textBytes:%d\n", textBytes);
      Serial.printf("strUTF16:%04x\n", strUTF16);
      Serial.printf("plotText:%d:%s\n", j,
                    plotText.substr(j, textBytes).c_str());
      // 複数バイト文字の場合ポインタをずらす
      j += textBytes;
      for (int i = 0; i < sizeof(fontIndex) / sizeof(fontIndex[0]); i++) {
        if (strUTF16 == fontIndex[i]) {
          // マルチバイト文字の場合textBytes分だけ関数に渡す
          drawFont(fontDataIndex[i], textBytes);
          break;
        }
      }
    }
  }
}
```



上位3bitをflag、下位5bitをdataとして扱います
なのでflagと命令の種類を==で判定します

```cpp
void drawFont(const int *fontData, int textWidth) {
  if (fontData == NULL) {
    return;
  }
  int index = 0;
  float x = 0;
  float y = 0;

  while (fontData[index] != 0x20) {

    uint16_t flag = fontData[index] & 0b11100000;
    uint16_t data = fontData[index] & 0b00011111;

    if (flag == 0x20) {
      // Xへ横移動
      // move toX

      if (6 < data) {
        // 0x27が飛んでいる分引く
        data--;
      }
      // 0x21が0なので引く
      data--;

      Serial.printf("Move toX=%d ", data);
      // 移動するのでペンを上げる
      penUp();
      moveTo(x , y);
    } else if (flag == 0x40) {
      // 横移動のみで線を引く
      if (27 < data) {
        // 5Bから5Eまで飛んでいるので引く
        data -= 2;
      }
      Serial.printf("Draw from here toX=%d\n", data);
      // 線を引くのでペンを降ろす
      penDown();
      moveTo(x, y);
      _GRBL.WaitIdle();
    } else if (flag == 0xA0 || fontData[index] == 0x7E) {
      // 0x60終点Xとして終点Yへ移動する
      // Move toY
      if (fontData[index] == 0x7E) {
        // 0x7Eの例外処理
        Serial.printf("toY=0\n");
        data = 0;
      } else {
        Serial.printf("toY=%d\n", data);
      }
      // 移動するのでペンを上げる
      penUp();
      moveTo(x, y);
      _GRBL.WaitIdle();
    } else if (flag == 0x60) {
      // 線を引く終点Xを指定
      Serial.printf("Draw toX=%d ", data);
      x = data;
    } else if (flag == 0xC0) {
      // 0x60を終点Xとして終点Yへ線を引く
      // Draw toY
      Serial.printf(" toY=%d\n", data);
      penDown();
      moveTo(x, y);
      _GRBL.WaitIdle();
    }
    index++;
  }
}
```

これで1文字書けるようになります


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">M5Stackペンプロッタでストロークフォントに対応しました<br><br>とりまAが描けたので明日以降複数文字を実装します(´・ω・`) <a href="https://t.co/MbrAkp2Px3">pic.twitter.com/MbrAkp2Px3</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1438196846673891337?ref_src=twsrc%5Etfw">September 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##複数文字に対応する##
複数文字を描画する場合は全角半角を判別してX軸のオフセットを管理すれば書けるようになります

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> ペンプロッタがストロークフォントの全角文字に対応しました＼(^o^)／ <a href="https://t.co/x7hjYQdzzX">pic.twitter.com/x7hjYQdzzX</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1439655917167865857?ref_src=twsrc%5Etfw">September 19, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

詳しくはソースをご覧ください(丸投げ)

```cpp
#include <ArduinoJson.h>
#include <M5Stack.h>
#include <string.h>

#include "GrblControl.h"
#include "fontData.h"

#define STEPMOTOR_I2C_ADDR 0x70
#define SERVO_ADDR 0x53
GRBL _GRBL = GRBL(STEPMOTOR_I2C_ADDR);

int degree = 0;

int PEN_UP = 130;
int PEN_DOWN = 107;

float x = 0.0;
float y = 0.0;

M5TreeView tv;

// addr 0x11 means "control the number 1 servo by angle"
void Servo_write_angle(uint8_t number, uint8_t angle) {
  Wire.beginTransmission(SERVO_ADDR);
  Wire.write(0x10 | number);
  Wire.write(angle);
  Wire.endTransmission();
}

void penUp() {
  // servo(PEN_UP);
  Servo_write_angle(0, PEN_UP);
  delay(100);
  // myservo.detach();
}
void penDown() {
  Servo_write_angle(0, PEN_DOWN);
  delay(100);
  // myservo.detach();
}

void resetOrigin(char *axis) {
  _GRBL.SetMode("distance");  //　相対移動モード
  std::string str = "G1 1 F200";

  str.insert(3, axis);

  // USE Gcode
  _GRBL.Gcode(const_cast<char *>(str.c_str()));  // +1移動

  str = "G1 -0.3 F200";
  str.insert(3, axis);

  while (!_GRBL.InLock()) {
    _GRBL.Gcode(
        const_cast<char *>(str.c_str()));  // -0.2ずつロックされるまで移動
    delay(100);
  }

  Serial.println("Locked");
  Serial.println("Back to Origin");

  str = "G1 0.2 F200";
  str.insert(3, axis);

  while (_GRBL.InLock()) {
    _GRBL.UnLock();  // リミットスイッチのロックを解除する
    _GRBL.SetMode("distance");  // absoluteに戻るので再設定
    _GRBL.Gcode(const_cast<char *>(str.c_str()));
    delay(300);
    // スイッチが押しっぱなしだと即座にロックに戻るので移動してから少し待つ
  }

  _GRBL.SetMode("absolute");
  Serial.println("Moved to Origin");
}

void moveTo(float toX, float toY) {
  String command = "G1 X";
  command += String(toX);
  command += " Y";
  command += String(toY);
  command += " F200";
  Serial.println(command.c_str());
  _GRBL.Gcode((char *)(command.c_str()));
  _GRBL.WaitIdle();
}

void tap() {
  penDown();
  penUp();
}

int efontUFT8toUTF16(uint16_t *pUTF16, char *pUTF8) {
  // 1Byte Code
  if (pUTF8[0] < 0x80) {
    // < 0x80 : ASCII Code
    *pUTF16 = pUTF8[0];
    return 1;
  }

  // 2Byte Code
  if (pUTF8[0] < 0xE0) {
    *pUTF16 = ((pUTF8[0] & 0x1F) << 6) + (pUTF8[1] & 0x3F);
    return 2;
  }

  // 3Byte Code
  if (pUTF8[0] < 0xF0) {
    *pUTF16 = ((pUTF8[0] & 0x0F) << 12) + ((pUTF8[1] & 0x3F) << 6) +
              (pUTF8[2] & 0x3F);
    return 3;
  } else {
    // 4byte Code
    *pUTF16 = 0;
    return 4;
  }
}

int textOffset = 0;

void drawFont(const int *fontData, int textWidth) {
  if (fontData == NULL) {
    return;
  }
  int index = 0;
  float x = 0;
  float y = 0;
  float scaleX = 4.0;
  float scaleY = 20;
  float fontWidth = 16;

  if (1 < textWidth) {
    textWidth = 2;
  }

  fontWidth /= scaleX;

  while (fontData[index] != 0x20) {
    uint16_t flag = fontData[index] & 0b11100000;
    uint16_t data = fontData[index] & 0b00011111;

    if (flag == 0x20) {
      // Xへ横移動
      // move toX
      if (6 < data) {
        data--;
      }
      data--;
      Serial.printf("Move toX=%d ", data);
      x = data / scaleX;
      penUp();
      moveTo(x + fontWidth * textOffset, y);
    } else if (flag == 0x40) {
      // 横移動のみで線を引く
      if (27 < data) {
        data -= 2;
      }
      Serial.printf("Draw from here toX=%d\n", data);
      x = data / scaleX;.
      penDown();
      moveTo(x + fontWidth * textOffset, y);
      _GRBL.WaitIdle();
    } else if (flag == 0xA0 || fontData[index] == 0x7E) {
      // 0x60終点Xとして終点Yへ移動する
      // Move toY
      if (fontData[index] == 0x7E) {
        Serial.printf("toY=0\n");
        data = 0;
      } else {
        Serial.printf("toY=%d\n", data);
      }
      y = data / scaleY;
      penUp();
      moveTo(x + fontWidth * textOffset, y);
      _GRBL.WaitIdle();
    } else if (flag == 0x60) {
      // 線を引く終点Xを指定
      Serial.printf("Draw toX=%d ", data);
      x = data / scaleX;
    } else if (flag == 0xC0) {
      // 0x60を終点Xとして終点Yへ線を引く
      // Draw toY
      Serial.printf(" toY=%d\n", data);
      y = data / scaleY;
      penDown();
      moveTo(x + fontWidth * textOffset, y);
      _GRBL.WaitIdle();
    }
    index++;
  }
  penUp();
  textOffset += textWidth;
}

void setup() {
  M5.begin(true, true, true, true);

  _GRBL.Init(400, 400, 400, 30);
  _GRBL.SetMode("absolute");
  dacWrite(25, 0);  //　ノイズ対策

  delay(1000);
  penUp();
}

void loop() {
  M5.update();

  if (M5.BtnA.wasReleased()) {
    std::string plotText = "Hellow世界";
    Serial.printf("plotText:%s\n", plotText.c_str());
    Serial.printf("plotText.length():%d\n", plotText.length());

    for (int j = 0; j < plotText.length();) {
      uint16_t strUTF16;
      int textBytes =
          efontUFT8toUTF16(&strUTF16, (char *)(plotText.substr(j).c_str()));
      Serial.printf("textBytes:%d\n", textBytes);
      Serial.printf("strUTF16:%04x\n", strUTF16);
      Serial.printf("plotText:%d:%s\n", j,
                    plotText.substr(j, textBytes).c_str());
      j += textBytes;
      for (int i = 0; i < sizeof(fontIndex) / sizeof(fontIndex[0]); i++) {
        if (strUTF16 == fontIndex[i]) {
          drawFont(fontDataIndex[i], textBytes);
          break;
        }
      }
    }
  }
  if (M5.BtnB.wasReleased()) {
    resetOrigin("X");
    resetOrigin("Y");
  }
}


```