M5 STAMP C3を使用したペンダント型コントローラーを開発中です。

 

 

 

標準ファームはDCC鉄道模型コントローラーのDSAir2(デスクトップステーション製)向けのBluetoothキーボード仕様ですが、ファームを改編することで様々な機器で使用可能です。

 

BluetoothとWiFi経由で他の機器とも接続可能です。シリアル通信は使用出来ませんのでご注意下さい(キーボードと排他利用のためです。ファームの書き換えはUSB経由で行えます)。

 

部品表

  • M5Stamp C3(技適有)
  • ロータリーエンコーダスイッチ
  • キーボード(4x4)
  • oledパネル(0.91 128×32)
  • バッテリーケース(単3x4本)
  • アクリルパネルx1セット
  • スペーサーx 8セット
  • M2xL8平ビスx4本
  • コード、ヘッダーピン
  • (バンダ数量外)

 

スケッチ

  • HMX-P001-BT-DSAir2-R001
  • (後日公開)

回路図

  • 結線図のみです。

 


操作方法

 

【初回の使い方】

裏面の電源スイッチをONにする。

 

スマートフォンまたはタブレットをDSair2と接続する。

 

スマートフォンまたはタブレットのBluetoothを使用可能にして、デバイスの追加およびペアリングを行う。ペンダントはESP32 keyboardと表示される。

 

DSair2の線路電源がONであれば、ダイアルを右回転させて、速度表示が変化することを確認する。変化すれば接続されている。

 

線路電源がOFFであればキーボードから「99#」と入力する。線路電源が入れば接続されている。

 

【運転方法】

速度操作

ダイアルを右回転することで、DSair2に加速の指示が出される。早く回すと多く加速する。ダイアルを左回転することで、減速の指示が出される。ダイアルを押すと、停止の指示が出される。

 

線路電源

キーボードから「99#」と入力すると、線路電源を入り切りできる。

 

方向

キーボードの「※」キーを押すと進行方向を反転する。「88# 」も同じ効果。

 

ファンクション

キーボードから0〜29の数字を入力し、「#」またはA〜D のキーを押すことで、DSair2に当該のファンクションをONまたはOFFする指示がでる。A〜Dは数字をメモリするので、次回はA〜Dのキーを押すことファンクションのON/OFFを指示できる。

 

キーボード入力

数字を打ち間違えた場合は適当な数字をいれて3桁の数字にする操作をしてください。入力がリセットされます。

 

【終わり方】

裏面の電源ボタンをOFFにする。

 

【2回目以降の使い方】

ペアリング操作は不要です。

 


【FAQ】

Q1:Bluetooth keyboardは接続しているのに、反応しない。

 

A1:スマートフォンまたはタブレットのBluetoothをOFF→ONして再接続してください。


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ご無沙汰しております。
 
最近はDesktopStationさんのスマイルサウンドデコーダで遊んでいて、自主開発が疎かになっていますが、ひさびさに自作をしましたので、ブログに残したいと思います。
 
<写真右下の”しょうゆいれ”が BT Throttle for DSair2 Miniです。>
 
ことの始まりは、ゆうえん・こうじさん(@JGR1067)がDSair2用のUSBコントローラについて呟いておられるのを見たことでした。
 
家にある材料でなんとかできないかなぁ?と考えてみると、前にDCC++EX用に作ったM5 Stamp C3(ESP32C3)用のコントローラをBTキーボードとして使うと同じ事ができそうでした。
 
<写真右が DCC++EX用コントローラ>
 
使ってみると、エネループ×3本では持ちが悪く、エネループ×4本への換装が必要でした。ちょうどよいケースを求めて、秋葉原を徘徊するも良いものがなく、百均を徘徊して見つけたのが、”しょうゆいれ”でした。
 
 
上蓋に、スイッチ類を移設し、電池ケースにM5 Stamp C3とディスプレイを張り付け、配線をして完成です。
 
 
 

 

 

 

スイッチの穴をロータリエンコーダと同心円で開けたのが失敗(もっと外側にしたほうが使いやすい)でしたが、なんとか使えています。

 

参考に回路図とスケッチを載せておきます。

せっかく、BTとWifiが使えるESP32C3なので、DSair2のWifiネイティブ、DCC++EX、wiThrotteにも

対応させていきたいと思います。

 

ご覧いただきありがとうございました。

 
 
回路図
 

スケッチ

//       BT Throttle for DSair2                    //
// 2022-09-23 HMX@Tetsuo-san Returns //
//    public-domain software                 //

#include <RotaryEncoder.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <BleKeyboard.h>

BleKeyboard bleKeyboard;

#define LED_PIN 2
#define Enc_1A_PIN 4
#define Enc_1B_PIN 5
#define Enc_2A_PIN 6
#define Enc_2B_PIN 7
#define SW_1_PIN 10
#define SW_2_PIN 18

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET    -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32

// create a pixel strand with 1 pixel on PIN_NEOPIXEL
Adafruit_NeoPixel pixels(1, LED_PIN);

// Encoder //
RotaryEncoder* encoder1 = nullptr;
RotaryEncoder* encoder2 = nullptr;

IRAM_ATTR void checkPosition() { 
    encoder1->tick(); // just call tick() to check the state.
    encoder2->tick(); 
}

// OLED Display //
  Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// 変数 LOCO //
  int Cmd_MaxNum = 32;
  char* Cmd_Caption[] = {"Power","STOP","FWD/REV"
                        ,"F0","F1","F2","F3","F4","F5","F6","F7","F8","F9"
                        ,"F10","F11","F12","F13","F14","F15","F16","F17","F18","F19"
                        ,"F20","F21","F22","F23","F24","F25","F26","F27","F28"};
  int Cmd_Tab[] = { 15, 20, 5
                            , 32, 32, 32, 32, 32, 32, 32, 32, 32, 32
                            , 28, 28, 28, 28, 28, 28, 28, 28, 28, 28
                            , 28, 28, 28, 28, 28, 28, 28, 28, 28};

void setup() {

// Serial //
  Serial.begin(115200);

// pixel //
   pixels.begin();  // initialize the pixel
  
// OLED Display //
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
      pixels.setPixelColor(0, pixels.Color(126,0,0));
      pixels.show();
      for(;;); // Don't proceed, loop forever
    } else {
      pixels.setPixelColor(0, pixels.Color(0,15,0));
      pixels.show();
    }
    display.clearDisplay();
    display.display();

  delay(100);    

// Encoder //  必ずOLEDより後に入れる。  
    encoder1 = new RotaryEncoder(Enc_1A_PIN, Enc_1B_PIN, RotaryEncoder::LatchMode::TWO03);
    attachInterrupt(digitalPinToInterrupt(Enc_1A_PIN), checkPosition, CHANGE);
    attachInterrupt(digitalPinToInterrupt(Enc_1B_PIN), checkPosition, CHANGE);

    encoder2 = new RotaryEncoder(Enc_2A_PIN, Enc_2B_PIN, RotaryEncoder::LatchMode::TWO03);
    attachInterrupt(digitalPinToInterrupt(Enc_2A_PIN), checkPosition, CHANGE);
    attachInterrupt(digitalPinToInterrupt(Enc_2B_PIN), checkPosition, CHANGE);

// Switch //   
     pinMode( SW_1_PIN, INPUT_PULLUP );
     pinMode( SW_2_PIN, INPUT_PULLUP );

// bBleKeyboard //   
     bleKeyboard.begin();
     
}

void loop() {

// 変数定義 //
    static unsigned long SpeedMillis = millis();
    static boolean ValueChange = true;
    
    static int pos1 = 0;
    static int Ex_pos1 = 0;
    static int Opos2 = 0;
    static int Ex_pos2 = 0;
    
    static int XSW1 = 0;
    static int XSW2 = 0;

    int SW1 ;
    int SW2 ;

    static int Cmd_Num ;
    static int BT_Flag = 0;

// 状態取得 //
    encoder1->tick(); // just call tick() to check the state.
    encoder2->tick(); 

    int newPos1 = encoder1->getPosition();
    int newPos2 = encoder2->getPosition();
    
    SW1 = digitalRead(SW_1_PIN);
    SW2 = digitalRead(SW_2_PIN);

    if(bleKeyboard.isConnected() and BT_Flag == 0) {
      ValueChange = true;   
      BT_Flag = 1;
    }
// イベント処理 //

//  スイッチ1 //
    if (XSW1 != SW1 and SpeedMillis + 100 < millis()) { 
        bleKeyboard.print("2"); 
        XSW1 = SW1;
        ValueChange = true;
        SpeedMillis = millis();    
    }
//  スイッチ2 //
    if ( XSW2 != SW2 and SpeedMillis + 100 < millis()) {
      if ( SW2 == 1 ) {
        function_call(Cmd_Num);
        }
        delay(100);
        XSW2 = SW2;
        ValueChange = true;     
        SpeedMillis = millis();
      }


// マスコンダイアル   
    if (Ex_pos2 != newPos2) {
      if(bleKeyboard.isConnected()) {

        if ((newPos2-Ex_pos2) > 8 ) { // 高速移動
          bleKeyboard.print("c");  
          Serial.println("c"); 
        } else if ((newPos2-Ex_pos2) > 0 ) {
          bleKeyboard.print("x");  
          Serial.println("x"); 
        } else if ((newPos2-Ex_pos2) < -8 ) { // 高速移動
          bleKeyboard.print("d"); 
          Serial.println("d"); 
        } else if ((newPos2-Ex_pos2) < 0 ) {
          bleKeyboard.print("s");
          Serial.println("s"); 
        }
      bleKeyboard.releaseAll();
      // bleKeyboard.print("Hello world");
      // bleKeyboard.write(KEY_RETURN);
      // bleKeyboard.write(KEY_MEDIA_PLAY_PAUSE);
      // bleKeyboard.press(KEY_LEFT_CTRL);
      // bleKeyboard.press(KEY_LEFT_ALT);
      // bleKeyboard.press(KEY_DELETE);
      }
        Ex_pos2 =  newPos2 ;
        ValueChange = true;     
    }

// セレクタダイアル    
    pos1 =  (newPos1+1)*0.5 ;
    if (Ex_pos1 != pos1) {
        Cmd_Num -= (pos1 -Ex_pos1) ;
        if (Cmd_Num < 0) { 
          Cmd_Num = Cmd_MaxNum -1 ; 
        }
        if (Cmd_Num > Cmd_MaxNum - 1 ) { 
          Cmd_Num = 0 ; 
        }
        Ex_pos1 = pos1 ;
        ValueChange = true;     
    }
    
// 処理実行 //
    if ( ValueChange ) {
        display.clearDisplay();
        Draw_Meter(Cmd_Num);
        display.display();
        ValueChange = false;
    }
}

void Draw_Meter(int Cmd_Num) {
        display.fillRect(  0, 26, SCREEN_WIDTH, 40, WHITE ) ;
        
        // Status //
        display.setTextSize(1);             
        if(bleKeyboard.isConnected()) {
          display.fillRect(  0,  0, SCREEN_WIDTH, 14, WHITE ) ;
          display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
          display.setCursor(23,5);
          display.print("BT Connected");
        } else {
          display.fillRect(  0,  0, SCREEN_WIDTH, 14, WHITE ) ;
          display.fillRect(  5,  2, SCREEN_WIDTH-10, 10, BLACK ) ;
          display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); 
          display.setCursor(17,4);
          display.print("BT Not Connected");
        }
  
        // Select //
        display.setTextSize(2);             
        display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
        display.setCursor(20 + Cmd_Tab[Cmd_Num],40);
        display.print(Cmd_Caption[Cmd_Num]);
}

void function_call(int cmdnum){

// 後日、スマートに書き直す //

          switch (cmdnum) {
          case 0:
              bleKeyboard.print(" "); //電源
            break;
          case 1:
              bleKeyboard.print("q"); //停止 
            break;
          case 2:
              bleKeyboard.print("z"); //方向
            break;
          case 3:
              bleKeyboard.print("0"); //以下ファンクション F0-F28 
            break;
          case 4:
              bleKeyboard.print("1");
            break;
          case 5:
              bleKeyboard.print("2");
            break;
          case 6:
              bleKeyboard.print("3");
            break;
          case 7:
              bleKeyboard.print("4");
            break;
          case 8:
              bleKeyboard.print("5");
            break;
          case 9:
              bleKeyboard.print("6");
            break;
          case 10:
              bleKeyboard.print("7");
            break;
          case 11:
              bleKeyboard.print("8");
            break;
          case 12:
              bleKeyboard.print("9");
            break;
          case 13:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("0");
              bleKeyboard.releaseAll();
            break;
          case 14:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("1");
              bleKeyboard.releaseAll();
            break;
          case 15:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("2");
              bleKeyboard.releaseAll();
            break;
          case 16:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("3");
              bleKeyboard.releaseAll();
            break;
          case 17:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("4");
              bleKeyboard.releaseAll();
            break;
          case 18:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("5");
              bleKeyboard.releaseAll();
            break;
          case 19:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("6");
              bleKeyboard.releaseAll();
            break;
          case 20:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("7");
              bleKeyboard.releaseAll();
            break;
          case 21:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("8");
              bleKeyboard.releaseAll();
            break;
          case 22:
              bleKeyboard.press(KEY_LEFT_SHIFT);
              bleKeyboard.print("9");
              bleKeyboard.releaseAll();
            break;
          case 23:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("0");
              bleKeyboard.releaseAll();
            break;
          case 24:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("1");
              bleKeyboard.releaseAll();
            break;
          case 25:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("2");
              bleKeyboard.releaseAll();
            break;
          case 26:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("3");
              bleKeyboard.releaseAll();
            break;
          case 27:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("4");
              bleKeyboard.releaseAll();
            break;
          case 28:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("5");
              bleKeyboard.releaseAll();
            break;
          case 29:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("6");
              bleKeyboard.releaseAll();
            break;
          case 30:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("7");
              bleKeyboard.releaseAll();
            break;
          case 31:
              bleKeyboard.press(KEY_LEFT_CTRL);
              bleKeyboard.print("8");
              bleKeyboard.releaseAll();
            break;

          }
}

 

 

かなり間が空いてしまいましたが、完成しました!

 

再生できる音の種類は次です。
 ブロア(起動、発車、運転中、停車、停止)
 モータ(発車、定速、減速、再加速)
 ホイッスル(長、短)
 ベル
 レールスキール音
 ブレーキ音
 

[KatoのShb電源荷物車] 

この車両に入れ込みました。

 

[回路全景] 

左からスピーカー、基板(アンプ部、電源部、DCC信号分離部)、XIAO RP2040になります。

 

[フィッティング1]

XIAO RP2040は端子部をゴリゴリとやすりで削りました。幅13mmぐらいです。

 

[フィッティング2]

放熱を考えてレギュレータを設置します。ウエイトに熱を逃がす様に設置します。

 

[BOM]

材料費@1048円は、なかなかお安いのではないかと思います。

 

[回路図] (K-CAD6.0で作図。まだうまく書けません)

 

[回路概要] (K-CAD6.0で作図。まだうまく書けません)

 

[スケッチ/DCC]

DCC信号の受信・解読はNmraDccMultiFunctionDecoder_1

を使用しました。 (ありがとうございます。)

 

 

[スケッチ/Sound]

音声再生はフジガヤさんの "timer_test2_752_31.zip"を使用しました。 (ありがとうございます。) 

 

スケッチは音量や途中終了など色々と直しています。音声ファイルはフラッシュ容量の1Mに収まるように16kHzに固定しました。