t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Arduino Pro MicroでPS4コントローラーをマクロキーボードとして使う(小型化)

今回は以前に紹介したPS4コントローラーをマクロキーボードとして使う件の小型化について紹介する。
thom.hateblo.jp

f:id:t-hom:20201121010920p:plain

わざわざ私が書かなくても、Arduino Pro MicroとミニUSB Host Sheildを組み合わせた事例はネットで検索できるが、ハマりどころが2か所ほどあったのでそこを強調する意味で記事にしようと思う。

サイズ感

左が従来型で右が今回作成したもので、機能的には全く同じである。
f:id:t-hom:20201120214349p:plain
これでケーブルの向きも良い感じになった。

購入したもの(要注意)

Arduino Pro Micro 3.3V 8MHz

上記は5個セットだが、単品でも購入できるかと思う。
Arduino Pro Microには次の2種類があるので、購入時に要注意である。
今回の目的で使用する場合は、必ず3.3V版を利用する必要がある。

動作電圧 動作クロック
5V版 5V 16MHz
3.3V版 3.3V 8MHz

見た目はほぼ同じで、ルーペで確認しないと分からないレベルなので形で判断はできない。
私は間違えて5V版の5個セットを購入してしまい、全く動作しなかった。

同じページから購入しても2個セットを選択したら5V版だったりするので改めて購入しようとする商品が3.3V版であることを確かめる必要がある。

私が買った商品のAmazon説明では3.3vの16MHzになっていたが、実際には3.3vの8MHzが届いた。
ちなみに表記間違いはAmazonへレポート済である。

なお、単品購入の場合は同じメーカーのものは見つからないかもしれないが、Arduino Pro Microと書いてあって3.3Vで部品の配置が同じだったらメーカーが違えど基盤の色が違えど動作するはず。(自己責任でお願いします。)
大事なことなので何度も繰り返すが、5V版と3.3V版は部品配置もカラーリングも同じなので表記で見分けるしかない。

※拡大写真があれば、水晶発振器に8MHzと書いてあるはずだが、両方販売してる場合に画像を使いまわしてる場合もあるので過信はできない。

届いてから見分ける方法としては、電圧レギュレーターの部品型番と水晶発振器のクロック数表記が違う。
f:id:t-hom:20201120220447p:plain
ルーペがあるなら個包装を開封する前に確認すれば返品できるかもしれない。

ミニUSB ホストシールド

こちらはPrimeじゃなかったので海外から到着するのに約3週間かかった。
単品でPrime商品もあるのでそっちで良いと思う。
これもメーカー違いが複数出てるけど商品画像の部品配置が同じだったら商品も同じだと思う。(自己責任でお願いします。)

加工

パターンカット

今回は3.3V版のArduino Pro Microを使うが、その理由はミニUSBホストシールドのチップが3.3Vにしか対応していない為である。ただそのままだとUSBに給電される電圧も3.3Vなので、通常5Vで動作する機器が動作しなくなる。よって別の場所から5Vを給電するために3.3Vの給電線をカットするという作業が必要になる。

下図に赤線で示したところにカッターナイフで何度も傷をつけて、基盤パターンをカットする。
f:id:t-hom:20201120222155p:plain

下図赤丸で示す抵抗の端のハンダと、もうひとつの赤丸のスルーホールにそれぞれテスターの針を当てて導通してなければ正しくカットできている。
f:id:t-hom:20201120222451p:plain

実際に測っているところ。
f:id:t-hom:20201120223055p:plain

カット跡はこんな感じ。
f:id:t-hom:20201120223207p:plain

ArduinoとUSB Host Shieldの接続

まずは接続箇所を説明し、実際の接続手順はその後に解説する。

接続関係の説明

基本的にはUSB接続口が左右逆向きになるように重ね合わせることができるが、下図の赤×印の部分は上下のホールを繋いではいけない。また、赤線、青線はそれぞれワイヤーで接続する必要がある。
f:id:t-hom:20201120224928p:plain

まず赤線は、ArduinoのRawからUSBホストシールドのUSB接続口へ給電するためのケーブルである。
Rawは生という意味で、Arduino自体のUSBから給電された生の5Vが直接出力されている。これは3.3V版を購入しても同様なので、この電圧をUSB機器へ直接流すことで5V駆動の周辺機器を繋ぐことができる。

青線はArduinoのリセットとUSBホストシールドのリセットを繋ぐケーブルである。

そしてUSBホストシールド側の上段の2つの×印はArduino側ではそれぞれのケーブルが繋がれる位置のため、何も接続しない。
下段の×印はよく見るとリセットと接続されていることが分かると思う。そしてArduino側はGNDになっている。リセットとGNDを接続してしまうと機器にリセットがかかってしまうので、ここは接続しない。

接続手順

接続にはArduino Pro Microに付属する12ピンを利用する。
f:id:t-hom:20201120231316p:plain

このうち1つは、端から3つ目のピンをラジオペンチで抜き取っておく。これは先ほど説明したArduinoのGNDとUSB Host Shieldのリセットを接続させないための処置である。
f:id:t-hom:20201120231510p:plain

ちなみに、ピンを抜く代わりに基盤のパターンをカットすると紹介されているページもあった。どちらの方法でも、GNDとRSTを接続させないという目的は達成できるのでOK。
f:id:t-hom:20201120233148p:plain

さて、もう一方はラジオペンチで9、1、2に分けて、このうち2は破棄する。
f:id:t-hom:20201120231855p:plain

ミニブレッドボードに次のように差し込む。USB側はどうしても浮き上がった形になってしまうが仕方がない
f:id:t-hom:20201120232047p:plain

上段の×印は接続させたくないだけなので、ピンを抜いただけで黒い土台は残している。
下段の×印はそこにケーブルが収まるので土台もない状態。

上からArduino Pro Microを重ねる。
f:id:t-hom:20201120232522p:plain

そしてピンのある個所だけを半田付けする。

半田付けできたら一旦ブレッドボードから抜いて分離する。
f:id:t-hom:20201121001344p:plain

次に、ワイヤーをUSB Host Shieldのスルーホールに半田づけする。
赤線はパターンカットしたUSB給電へ。青線はそこから2つ飛ばしてRSTへ接続する。
裏側から半田付けするが、ケーブルが途中で抜けないようにマスキングテープを活用する。
f:id:t-hom:20201121001534p:plain

次に、赤線をArduinoのRawに、青線をArduinoのRSTへ接続する。この時ケーブルが長すぎるとこのあと2つの基盤を重ねた時に間に収まらなくなるので、以下の写真くらいの長さにとどめておく。
f:id:t-hom:20201121001853p:plain

表からみた写真。
f:id:t-hom:20201121002004p:plain

次に、2つの基盤を重ね合わせる。完全に差し込む前に、ワイヤーの根本をラジオペンチなどでなるべく奥側へ倒しておく(下図矢印の方向)。半田付けされているので割と固いけど押し込んでおかないと基盤を合体させたときに最後までささらない。
f:id:t-hom:20201121002320p:plain

最後まで押し込むとコンパクトにピッタリ収まる。
f:id:t-hom:20201121003111p:plain

次に基盤どおしが浮かないようにマスキングテープで固定する。
f:id:t-hom:20201121003156p:plain

そしてマスキングテープが無い場所からハンダ付けをして、マスキングテープをはがして残りのピンをハンダ付けする。
※ピンが出ていないところはハンダ付けしないので注意。

これで接続は完成。
f:id:t-hom:20201121003347p:plain

参考サイト

https://ht-deko.com/arduino/shield_usbhost_mini.htmlht-deko.com
参考というかまるっとそのまま真似したのだが、初心者の私にとって細かいニュアンスで色々とつまずく点もあったので今回こちらでも解説してみた。

プログラム書き込み

プログラムの書き込みでも重要な注意点がある。
5V版のArduinoでは単にボード選択でLeonardoを選べば良いが、今回使用する3.3V版はLeonardoとして書き込んでしまうと不具合が発生して動かなくなる。復旧は少し面倒なので間違えないようにしたい。

まずボードが登録されていないと思うので環境設定から追加ボードマネージャーのURLを追加しておく。
f:id:t-hom:20201121003732p:plain

追加するURLは次のとおり

https://raw.githubusercontent.com/sparkfun/Arduino_Boards/master/IDE_Board_Manager/package_sparkfun_index.json

ツール→ボード→ボードマネージャーを開き、SparkFun AVR Boards by SparkFun Electronicsをインストールしておく。
f:id:t-hom:20201121004246p:plain

ツールから、ボードをSparkFun Pro Micro、プロセッサをATMega32U4(3.3V, 8MHz)を選択する。ここでプロセッサの電圧と周波数の選択を間違えるとまた苦労するので注意。
f:id:t-hom:20201121004523p:plain

あとはシリアルポートを選択し、普通に書き込むだけである。

トラブルシューティング

違うボードを選択して書き込んでしまった場合、リセット処理が必要になる。
まずArduino IDEでは正しいボードを選びなおしておく。
それから何も追加コードを書いていない新規のArduinoファイルで書き込み処理を開始し、コンパイルが開始された直後にGNDとRSTをすばやく2回ショートさせる。そうするとArduinoは数秒間だけ書き込み可能になる。ちょうどコンパイルが終わって書き込み開始される頃にリセットが完了している必要があり、シビアなタイミングが要求される。というか運ゲーである。10回に1回くらいは成功するので、さほど悲観に暮れる必要はないが面倒なのでボード選択は間違えないようにしたい。

GNDとRSTをショートさせるのはハンダで輪っかを作るとやりやすい。
f:id:t-hom:20201121010300p:plain

コード

前回から少しバージョンUPして、CTRL+マウスホイールによるズームイン・ズームアウトができるようになったのでコードを掲載しておく。コントローラー中央のプレイステーションボタンを押しながら右ジョイスティックを時計回りに回すとズームイン・半時計回りならズームアウトというコードになっている。

また、画面スクロールもアナログレバー(L2・R2)に割り当てた。押し込み具合によってスクロール量が変わる。

#include <Keyboard.h>
#include <Mouse.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

// #include "Arduino.h"
// #include "DFRobotDFPlayerMini.h"
// DFRobotDFPlayerMini myDFPlayer;

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;
uint8_t oldRightHatX, oldRightHatY;
uint8_t redundant;

void setup() {
  if (Usb.Init() == -1) {
    while (1); // Halt
  }
  Mouse.begin();
  redundant = 1;
  /* Serial1.begin(9600);
  if (!myDFPlayer.begin(Serial1)) {  //Use softwareSerial to communicate with mp3.
    while(true);
  }
  myDFPlayer.volume(30);  //Set volume value. From 0 to 30
  */
}

void loop() {
  Usb.Task();

  if (PS4.connected()) {
    if (PS4.getButtonPress(PS)&& ((abs(128 - PS4.getAnalogHat(RightHatX)) + abs(128 - PS4.getAnalogHat(RightHatX))) >= 127)) {
      //TopRight
      if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //BottomRight
      if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //BottomLeft
      if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //TopLeft
      if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
    }
    oldRightHatX = PS4.getAnalogHat(RightHatX);
    oldRightHatY = PS4.getAnalogHat(RightHatY);
    
    if (PS4.getAnalogButton(R2)) {
      Keyboard.press(KEY_DOWN_ARROW);
      delay(256-PS4.getAnalogButton(R2));
      Keyboard.releaseAll();
    }
    
    if (PS4.getAnalogButton(L2)) {
      Keyboard.press(KEY_UP_ARROW);
      delay(256-PS4.getAnalogButton(L2));
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(PS)) {
      
    }
    
    if (PS4.getButtonClick(TRIANGLE)) {
      Keyboard.press('a');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CIRCLE)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press('1');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(SQUARE)) {
    }

    if (PS4.getButtonPress(UP)) {
      Keyboard.press(KEY_PAGE_UP);
      delay(40);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(RIGHT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('.');
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonPress(DOWN)) {
      Keyboard.press(KEY_PAGE_DOWN);
      delay(40);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(LEFT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(',');
      delay(100);
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(L1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(',');
      delay(100);
      Keyboard.releaseAll();
      //myDFPlayer.play(1);
    }
    if (PS4.getButtonClick(L3)) {
      
    }
    if (PS4.getButtonClick(R1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('.');
      delay(100);
      Keyboard.releaseAll();
      //myDFPlayer.play(2);
    }
    if (PS4.getButtonClick(R3)) {
    }
 
    if (PS4.getButtonClick(SHARE)) {
    }
    if (PS4.getButtonClick(OPTIONS)) {
    }
    if (PS4.getButtonClick(TOUCHPAD)) {
    }
  }
}

以上

Arduino UNOで複数の環境センサー(温度、湿度、気圧、PM2.5、CO2)からデータ取得

今回はマイコンボードArduinoを使って複数の環境センサーからデータを取得するコードを紹介。この記事はシリアルモニター上で観測するところまでなので、これだけで普段使いのツールを作れるというものではないことはご承知いただきたい。

作成の動機

在宅勤務が続くなか、出社時と同等またはそれ以上のパフォーマンスを出すためには環境の見直しが重要だと考えた。オフィスでは事務所衛生基準規則等によって会社側が環境を整えてくれるが、在宅だと自分でなんとかする必要がある。よって、常時環境を監視して基準を外れた場合は対策を促すアラートを出すような仕組みを構築したいと思った。

既製品ではなく自作する動機としては、取得したデータを自分の思い通りに活用したい為。

記事にした動機

センサー単品のサンプルは探せばいくらでも出てくるが、複数取り扱うとなると一気に参考サイトが減る。センサー類の特性によっては読み取り開始から終了までの待ち時間があったり、何十秒か連続で読み取りを継続しなければならなかったりといったプログラム上の制約が出てくるので、これらをうまく取り扱う方法を紹介したいと思った。

下準備

温度・湿度・気圧センサーのハンダジャンパー

購入した温度・湿度・気圧センサーはハンダジャンパーでArduino側の通信ピンを選択できるようになってるらしい。
チップ上の3接点のうち、左と中央をハンダで繋げておく。
f:id:t-hom:20201108103718p:plain
普通は、初期状態で左と中央が繋がっているらしくそれを前提にした解説が多いのだが、私の購入した製品はどこにもジャンパーされてなかった。

Arduinoへライブラリのインストール

ツール→ライブラリを管理からAdafruit BME280 Libraryをインストールしておく。
f:id:t-hom:20201108110154p:plain

配線図

※CO2センサーは端子のないところにつながっているように見えるが、実際にはそこに端子がある。Fritzingというツールで図を作成した際、このセンサーの別のバージョンしかデータを入手できなかったため代用した。
f:id:t-hom:20201108132112p:plain

Arduino UNO内蔵型ブレッドボードを利用して検証した実際の配線がこちら。
ごちゃってて何の参考にもならないと思うが一応。。
f:id:t-hom:20201108113845p:plain

コード

このコードは私が書いたというよりは、ただ参考サイトのコードを組み合わせただけ。
変数名とかも基本的にはそのままだし本当はそれぞれ処理を関数化したいけど一旦動いたところまでで記事にしておこうと思いとりいそぎ公開。後日リファクタリングしたい。

//for CO2
#include <SoftwareSerial.h>

//for BME
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define SEALEVELPRESSURE_HPA (1013.25)

//for CO2
SoftwareSerial swSer(8, 7);
uint8_t cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
uint8_t reset[9] = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
uint8_t res[9] = {};
uint8_t idx = 0;
bool flag = false;
uint16_t co2=0;

//for BME
Adafruit_BME280 bme;

//for DustSensor
int pin = 9;
unsigned long t0;
unsigned long ts = 30000; // 30000ms
unsigned long lowOc = 0;
float ratio = 0;
float concent = 0;

void setup() {
  //for Common
  Serial.begin(9600);

  //for CO2
  swSer.begin(9600);
  //swSer.write(reset,9);//calib
  //delay(60000UL);

  //for BME
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  //for DustSensor
  pinMode(9,INPUT);
  t0 = millis();
}

void loop() {
  uint16_t val=0;
  float t;
  swSer.write(cmd,9);

  Serial.println("--------------------------------");
  //BME
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println("*C");

  Serial.print("Pressure = ");
  Serial.print(bme.readPressure() / 100.0F);
  Serial.println("hPa");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println("%");

  //DustSensor
  t0 = millis();
  lowOc = 0;
  while((millis() -t0) <= ts) {
    lowOc += pulseIn(pin, LOW);
  }
  ratio = lowOc/(ts*10.0);
  concent = 1.1 * pow(ratio,3) - 3.8 * pow(ratio,2) + 520 * ratio + 0.62;
  Serial.println("Particulates = " + String(concent) + "pcs/0.01cf");
  
  
  delay(30000);

  //CO2
  while(swSer.available()==0){
    Serial.println("bad com");
  }
  while(swSer.available()>0){
    res[idx++]=swSer.read();
    flag=true;
  }
  idx = 0;
  if(flag){
    flag=false;
    co2 = 0;
    //delay(100);
    co2 += (uint16_t)res[2] <<8;
    //delay(100);
    co2 += res[3];
    t = res[4];
  }
  Serial.print("CO2 = ");
  Serial.print(co2);
  Serial.println("ppm");
}

組み合わせの解説

個別のセンサー処理部分は参考サイトの解説か、ご自身で調べていただくとして、組み合わせ部分だけざっくり説明する。
まず、各センサーごとにデータ取得のタイミングが異なる。即時取得できるセンサーばかりであれば単純に順次取得すれば良いのだが、各参考ページやコードを見ているとそうでもないので一工夫する必要がある。

以下に今回使用したセンサーごとのデータ取得タイミングを記す。

センサー名 データ取得タイミング
温度・湿度・気圧センサー 即時データ取得可能
ダストセンサー 30秒間連続で信号を監視し続け、そのうち信号がLowになっていた時間を累計することで計測
CO2センサー 観測開始のコマンドを発行してから60秒程度Waitしてからデータ取得

順次取得でもできなくはないが、CO2センサーが60秒ただ待ち続けるのは時間のロスなので待ってる間にダストセンサーのデータ収集をするというロジックにした。

タイムラインがこちら。
f:id:t-hom:20201108113230p:plain

実行結果

シリアルモニターを起動しておくとこんな感じでデータが取得できる。
f:id:t-hom:20201108114450p:plain

なおCO2センサーのキャリブレーションは特にやってないので値のズレは生じていると思う。CO2濃度が400ppmに近い屋外でキャリブレーションするらしいのだが、値が大きくずれた場所でやってしまうと逆に狂いが大きくなるらしいので下手にいじらず出荷状態で使っている。

記事にするかどうかは別として今後の改良・応用プラン

  • データ表示のタイミングをそろえる
  • コードのリファクタリング
  • Arduinoとセンサー類をまとめて格納するケースの作成
  • ラズパイへデータ送信してリアルタイムのグラフ表示
  • 電光掲示板に取得した情報を流す
  • 基準値越えをアラームで警告

以上

Arduino LOENARDOでPS4コントローラをマクロキーボードとして使う

今回は、PS4のDualShockコントローラーをマクロキーボードとして使うことに成功したのでご紹介。
絵面はこんな感じ。
f:id:t-hom:20201028201735p:plain

このコントローラーは本来Bluetoothに対応しているが、有線のほうがコードがシンプルになる気がしたのであえて有線で使っている。

マクロキーボードとは

キーボードショートカット等の複数キーの連続入力を一つのキーに割り当てることができるキーボードのこと。
在宅勤務を開始してすぐの頃に9キー登録できるタイプを購入して、Outlookのメールを読むときに重宝している。
例えば次のメールに移動したり、前のメールに移動したり、メールを削除したりといったOutlookに最初から用意されたキーボードショートカットを各マクロキーに割り当てて使用している。
thom.hateblo.jp

パソコン側からみた振る舞いは普通のキーボードなので特にセキュリティ的なリスクはなく、普通のキーボードを繋いでるのと同じ。
ただ会社で使ってると目立つので周辺機器持ち込みOKなユルい職場か、在宅勤務が主な活躍場所となると思われる。

今回作ったものと経緯

既存のマクロキーボードでも十分実用的なんだけど、使っているうちにさらに便利にしたくなってきた。
より誤入力しにくく、操作しやすい形状が良い。さらに卓上だけでなく手にホールドして使いたい。

そこでたどり着いた答えがゲームパッド。
今回作ったのは、単にゲームパッドをマクロキーボードとして使うための変換デバイスである。
専用のマクロ登録ツールなどは存在しないので、マクロは直接Arduino IDEでハードコーディングして基盤に書き込む形である。

もともと自作キーボードとかでArduinoが使われていたのを知っていたので、ゲームパッドを分解して埋め込んだらなんとかなるのではないかと考えていたら、別解としてゲームパッド信号をプログラムで処理してキーコードに変換する手段が見つかったのが今回作成した経緯。

活躍の場は更に絞られ、会社のオフィスで使うのは多分無理。。
真面目にメール閲覧してても遊んでる絵面にしか見えない。在宅専用デバイスである。

ただ今回の事例を応用すれば普通のキーボードをつないでマクロキーボードにすることもできるかと思う。

材料

主な材料

Arduino LEONARDO

UNOはKeyboardライブラリやMouseライブラリに対応しておらず、入力デバイスとして使用することができないのでLEONARDOを選択する。
互換品やNano等、小さい製品も使用できるかもしれないが試してないのでなんとも。


その他

  • USBケーブル 片側micro/片側TypeA を2本(Arduino用とPS4コントローラーのデータ通信用)
  • ハンダ

スキル

真似するだけなら、スキルというほどのスキルは要らない。
ハンダ付けをしたことがない方はYouTubeとかのハンダ付け動画を見よう見まねでやれば良い。
はじめてだと動画の見本のようにはうまくいかず、どちらかといえば「失敗例」に近い仕上がりになるだろう。
まぁつながればなんでも動くので、趣味でやってる分には気にしなくて良い。

Arduinoについては、IDEのインストール、マイコンへの書き込み、ライブラリのインストール、シリアルモニターへの出力ができるようになってれば十分。
入門書とか入門記事でオンボードLEDをチカチカさせられるくらいの経験でもなんとかなる。

作業

USB Host Shieldのハンダジャンパー

ジャンパーというのは、離れた電気回路間をつなぐ電線や端子・ピンのことを指すらしい。
つなぐかつながないかで回路の挙動を変更したりする、一種のスイッチのように使われることも多い。

ハンダジャンパーは下図を見ていただけると早いと思う。青丸の箇所が繋がる前のハンダジャンパー。赤丸が繋げたあと。単にハンダで2接点を繋いだだけ。
f:id:t-hom:20201028211735p:plain

青丸の位置はつなげないので注意。赤丸の部分だけジャンパーさせておく。

ピン曲がりの微修正

前評判どおり、USB Host Shieldは品質が今一つで、とどいた商品の足が曲がっているのでこれをArduinoに挿せる程度には微修正しておく。強い力をかけると折れるリスクがあるので、すこしづつ。

USB Host ShieldをArduinoに接続

ピン曲がりのため微修正したとしても苦労はする。うまくピンを指矯正しながら少しづつ差し込んでいくイメージ。
6ピン(3x2)が最後まで刺さったらOK。他のピンは最後までささらず、2~3ミリくらい接合部からピンが見えるけど、それでOK。

なお、UNOとLEONARDOはピン配置が違うのでそのままでは使えないという記事があったけど、Rev.3というバージョンから共通化されてるらしく、手持ちのLEONARDOではそのまま使えた。

コーディング

まずツール→ライブラリを管理から、USB Host Shield と検索してUSB Host Shield Library 2.0をインストールする。
次に以下のコードを貼り付け。
まだ私もサンプルを改造しながら試してるだけなので、不要なコードも色々残っている。
それでもまぁ一つ達成して熱があるうちに記事にしてしまわないと、あとで面倒になるので書いてしまう。

#include <Keyboard.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;

void setup() {
  if (Usb.Init() == -1) {
    while (1); // Halt
  } 
}

void loop() {
  Usb.Task();

  if (PS4.connected()) {
    if (PS4.getAnalogHat(LeftHatX) > 137 || PS4.getAnalogHat(LeftHatX) < 117 || PS4.getAnalogHat(LeftHatY) > 137 || PS4.getAnalogHat(LeftHatY) < 117 || PS4.getAnalogHat(RightHatX) > 137 || PS4.getAnalogHat(RightHatX) < 117 || PS4.getAnalogHat(RightHatY) > 137 || PS4.getAnalogHat(RightHatY) < 117) {
      
    }

    if (PS4.getAnalogButton(L2) || PS4.getAnalogButton(R2)) { // These are the only analog buttons on the PS4 controller
      
    }
    if (PS4.getAnalogButton(L2) != oldL2Value || PS4.getAnalogButton(R2) != oldR2Value) // Only write value if it's different
      PS4.setRumbleOn(PS4.getAnalogButton(L2), PS4.getAnalogButton(R2));
    oldL2Value = PS4.getAnalogButton(L2);
    oldR2Value = PS4.getAnalogButton(R2);

    if (PS4.getButtonClick(PS)) {
      
    }
    if (PS4.getButtonClick(TRIANGLE)) {
      
    }
    if (PS4.getButtonClick(CIRCLE)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press('1');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(SQUARE)) {
      
    }

    if (PS4.getButtonClick(UP)) {
      Keyboard.press(KEY_PAGE_UP);
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(RIGHT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('>');
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(DOWN)) {
      Keyboard.press(KEY_PAGE_DOWN);
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(LEFT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('<');
      delay(100);
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(L1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_PAGE_UP);
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(L3)) {
      
    }
    if (PS4.getButtonClick(R1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_PAGE_DOWN);
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(R3)) {
      
    }
 
    if (PS4.getButtonClick(SHARE))
      
    if (PS4.getButtonClick(OPTIONS)) {
    }
    if (PS4.getButtonClick(TOUCHPAD)) {
    }
  }
}


キーボードマクロの設定は、if (PS4.getButtonClick(●●)){ }の部分。
以下に例として抜き出した。

    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }

これは"Ctrl+d"のショートカットを×ボタンに割り当てている部分。
キーボードインプットについてはArduinoのKeyboardライブラリについて検索すればいくつか事例が見つかると思う。


ちなみにもともとのコードはファイル→スケッチ例メニューから以下を探すと新規コードウインドウで出てくるので、コントローラーのバイブ機能とかLED制御とかはそちらを参照すると良い。

f:id:t-hom:20201028215449p:plain
スケッチ例のコードは押したキーがシリアルモニターに表示されるようになっており、IDEのツールからシリアルモニターを起動しておかないと動かないので注意。PS4のほかに、JoystickのサンプルスケッチでPC用のゲームパッドが使えるといった記事があったけどLogicoolの手持ちのゲームパッドでは動かなかった。やはり種類も多いので製品ごとに作りが違う為だろうか。
その点有名なPS4コントローラーはサンプルスケッチもズバリ特化してるので間違いない。

Keyboardライブラリの使い方もざっくり知りたければスケッチ例から探すと良い。

使い方

この記事のコードはあくまで私のマクロ設定なので、コードは使いたいキーマクロに合わせて改造する必要がある。
一度Arduinoに書き込んでしまえばあとはどのPCにつないでも同じように動作する。
PS4コントローラーをUSB Host Shield側のUSBに接続し、Arduino側のUSBは普通にPCとつなぐ。
あとはボタン押すとArduino IDEでコーディングした通りのキーがPCに入力される。

Excel Tips アンケートや調査フォームでデータの入力規則をもう一工夫する。

Excelでアンケートや調査フォームを作成する際によく見かけるのが、入力規則を用いたドロップダウンリスト。

一般的に多いのが固定の選択肢が用意され、それ以外がエラーになる仕組みである。
f:id:t-hom:20200918211250p:plain


自由入力では設問の趣旨が伝わらず、トンチンカンな回答が返ってくる場合がある。
だからあらかじめ予想される回答を選択肢として用意し、そこから選択してもらおうという発想だ。
選択肢が十分に想定できる場合や、そもそも例外が存在しない場合はこれで良い。

しかしこの方法はドロップダウンリストにない項目は選択できないため、当てはまる選択肢がない場合に情報を取りこぼす恐れもある。

そこで私がたまに使うのは、基本的にはリストで選択肢を用意しつつ、手入力も受け付ける方法である。
f:id:t-hom:20200918212352p:plain

まぁ同じデータ入力規則でちょっと別のタブを触るだけなので、ひょっとして知ってる方も多いかもしれないがその割にこれまでの会社員経験で見かけたことがないので初めて知るという方も案外多いかもしれない。

設定箇所はデータ入力規則のエラーメッセージタブ。
デフォルトではスタイルは停止になっており、タイトルやメッセージは設定されていないが、これを警告にすると先に見せたように継続するかどうかを尋ねるプロンプトになる。タイトルやメッセージも適切なものを設定しておこう。
f:id:t-hom:20200918212910p:plain

あと、同じくデータ入力規則の入力時メッセージタブで設定できるメッセージも便利。
f:id:t-hom:20200918213140p:plain

これはどちらかといえばリストよりは、「すべての値」の時に使うことが多い。

データの入力規則機能は「規則」部分だけ注目されてる気がするが、その他の機能も意外と便利なので紹介してみた。

知らなかった!という方はこれを機に活用してみて欲しい。
そんなの知ってるし!という方は、それにしては活用例を見かけないので布教よろ。

以上

VBA Excelガントチャート作成マクロ

今回はVBAでExcelガントチャートを作成するマクロを紹介する。
作成したガントチャート自体はマクロに依存せずExcelの基本機能で動作する。

完成すると以下のようなイメージになる。
f:id:t-hom:20200712012937p:plain

大元のアイデアはこちらのYouTube動画を参考にしている。

動画だと英語の解説で結構操作スピードも速い。また、手動で作成しているので毎回再現するのも面倒だ。
テンプレートを作って使いまわしても良いが、それよりもいつでも再現できるVBAコードの形で残しておこうと思って今回マクロ化した。

オリジナルを参考にしつつ私が新たに追加した機能は次のとおり。

  • 現在進行中のタスクを赤い三角でマーク
  • "Phase"で始まるタスク名を太字と色で強調
  • 計画(PLANNED)と実績(ACTUAL)が入力でき、ガントチャートの方でもPLANNEDが背景塗りつぶし、ACTUALが「≫」で表示
  • 現在のSTATUSはPLANNEDとPROGRESSから自動入力され、Delayはオレンジ系、Over Dueは赤系の色で警告
  • テーマカラー使用の為、ページレイアウトの配色から簡単に好みの色合いに変更可能

使い方

コードが非常に長いので先に使い方を説明する。
マクロを実行すると新規ブックに次のようなフォームが作成される。
f:id:t-hom:20200712014604p:plain
テーマカラーを多用しているのでオフィスのバージョンによって異なると思われる。

次に以下の薄黄色で示した箇所を手入力する。(説明のために塗っただけで、実際は白背景)
f:id:t-hom:20200712015425p:plain

このときタスク名にPhaseで始まる名称を使用すると自動的に強調される。

タスクは手動でインデントするとより見やすくなる。
f:id:t-hom:20200712015644p:plain

ページレイアウトタブの配色から好きな色を選択する。
f:id:t-hom:20200712015740p:plain

あとはファイル名を付けて保存すれば完成。
作成されたガントチャートはVBAを使用しないのでxlsxで保存すればOK。

7/12 10:30 追記

土日及び祝日を網掛けする機能を追加した。祝日はAT列に手動で入力する想定。コードも修正済。
f:id:t-hom:20200712103101p:plain

7/25 22:00 バグ修正

Over Dueの計算式が間違っていたので修正

Before

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<=TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"

After

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"

コード

今回は条件分岐等が生じない単なる再現系マクロなので、マクロ記録に毛が生えた程度のコードである。
ある程度コード整理はしたものの、プロシージャ分割等は一切しなかった。

NUMBER_OF_TASKSの値がタスクの行数を表すので、ここを変えると任意のタスク数でガントチャートを作成できる。
他に変更を想定したパラメーターは特にない。
※ちなみにWEEKの値は1週間が7日であることを示す定数なので変更してはならない。

Sub CreateGantt()
    Const WEEK As Integer = 7
    Const NUMBER_OF_TASKS As Integer = 100
    
    '#General Setting
    Dim sh As Worksheet
    Set sh = Workbooks.Add.Sheets(1)
    ActiveWindow.DisplayGridlines = False
    With sh.Cells.Font
        .Name = "Meiryo UI"
        .Size = 9
    End With
    
    '#Header Setting
    sh.Range("A1").Value = "Input Project Name Here"
    With sh.Range("A1").Font
        .Size = 22
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = -0.25
    End With
    
    sh.Names.Add "R_ProjectStart", sh.Range("C3")
    sh.Names.Add "R_DisplayWeek", sh.Range("C4")
    
    With sh.Range("R_ProjectStart")
        .Value = Date
        .Offset(0, -1).Value = "Project Start:"
        .Offset(0, -1).HorizontalAlignment = xlRight
    End With
    
    With sh.Range("R_DisplayWeek")
        .Value = 1
        .Offset(0, -1).Value = "Display Week:"
        .Offset(0, -1).HorizontalAlignment = xlRight
    End With
    
    Dim headerCursor As Range: Set headerCursor = sh.Range("A7")
    Dim h
    For Each h In Split(",TASK,ASSIGNED TO,START,END,START,END,PROGRESS,STATUS", ",")
        headerCursor.Value = h
        Set headerCursor = headerCursor.Offset(0, 1)
    Next

    Dim dateCursor As Range: Set dateCursor = headerCursor.Offset(-1, 0)
    dateCursor.Formula = "=R_ProjectStart-WEEKDAY(R_ProjectStart)+1+((R_DisplayWeek-1)*7)"
    dateCursor.NumberFormatLocal = "d"
    With dateCursor.Offset(0, 1)
        .FormulaR1C1 = "=RC[-1]+1"
        .AutoFill Destination:=.Resize(1, WEEK * 5 - 1), Type:=xlFillDefault
    End With
    dateCursor.Resize(1, WEEK * 5).EntireColumn.ColumnWidth = 3
    dateCursor.Resize(2, 1).EntireRow.HorizontalAlignment = xlCenter
    
    Dim weekdayCursor As Range
    Set weekdayCursor = dateCursor.Offset(1, 0)
    With weekdayCursor
        .FormulaR1C1 = "=LEFT(TEXT(R[-1]C,""ddd""),1)"
        .AutoFill Destination:=.Resize(1, 7 * 5), Type:=xlFillDefault
    End With

    Dim weekCursor As Range: Set weekCursor = dateCursor.Offset(-1, 0)
    With weekCursor
        .FormulaR1C1 = "=R[1]C"
        .NumberFormatLocal = "yyyy/m/d;@"
        .Font.Size = 12
        With .Resize(1, WEEK)
            .Merge
            .HorizontalAlignment = xlLeft
            .AutoFill .Resize(1, WEEK * 5)
        End With
    End With
    
    'Paint
    Dim headerRange As Range
    Set headerRange = sh.Range(Cells(headerCursor.Row, 1), headerCursor.Offset(0, WEEK * 5 - 1))
    With headerRange.Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorLight1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
    End With
    headerRange.Font.Color = rgbWhite

    With weekCursor.Resize(1, WEEK * 5).Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
    End With
    
    With dateCursor.Resize(1, WEEK * 5).Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = 0.8
        .PatternTintAndShade = 0
    End With

    Dim r As Range
    Set r = weekCursor
    For i = 1 To 5
        With r.Resize(2, WEEK)
            .Borders(xlDiagonalDown).LineStyle = xlNone
            .Borders(xlDiagonalUp).LineStyle = xlNone
            With .Borders(xlEdgeLeft)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            With .Borders(xlEdgeTop)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            .Borders(xlEdgeBottom).LineStyle = xlNone
            With .Borders(xlEdgeRight)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            .Borders(xlInsideVertical).LineStyle = xlNone
            .Borders(xlInsideHorizontal).LineStyle = xlNone
        End With
        Set r = r.Offset(0, 1)
    Next
    
    Dim bodyRange As Range
    Set bodyRange = headerRange.Offset(1, 0).Resize(NUMBER_OF_TASKS)
    bodyRange.RowHeight = 16.5
    With bodyRange
        .Borders(xlDiagonalDown).LineStyle = xlNone
        .Borders(xlDiagonalUp).LineStyle = xlNone
        .Borders(xlEdgeLeft).LineStyle = xlNone
        With .Borders(xlEdgeTop)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
        With .Borders(xlEdgeBottom)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
        .Borders(xlEdgeRight).LineStyle = xlNone
        .Borders(xlInsideVertical).LineStyle = xlNone
        With .Borders(xlInsideHorizontal)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
    End With
    
    'Gantt Bar
    Dim ganttRange As Range
    Set ganttRange = weekdayCursor.Offset(1, 0).Resize(NUMBER_OF_TASKS, WEEK * 5)
    ganttRange.FormulaR1C1 = "=IF(AND(RC6<=R6C,R6C<=RC7),""≫"","""")"
    With ganttRange
        .HorizontalAlignment = xlCenter
        .VerticalAlignment = xlCenter
    End With
    With ganttRange.Font
        .Size = 16
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = -0.5
    End With
    
    With ganttRange
        .FormatConditions.AddColorScale ColorScaleType:=2
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        .FormatConditions(1).ColorScaleCriteria(1).Type = xlConditionValueLowestValue
        With .FormatConditions(1).ColorScaleCriteria(1).FormatColor
            .Color = 2650623
            .TintAndShade = 0
        End With
        .FormatConditions(1).ColorScaleCriteria(2).Type = xlConditionValueHighestValue
        With .FormatConditions(1).ColorScaleCriteria(2).FormatColor
            .Color = 10285055
            .TintAndShade = 0
        End With
        .FormatConditions.Add Type:=xlExpression, Formula1:="=AND($D8<=J$6,J$6<=$E8)"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Interior
            .PatternColorIndex = xlAutomatic
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0.25
        End With
        .FormatConditions(1).StopIfTrue = False
    End With

    'Progress Data Bar
    With sh.Range("H8").Resize(NUMBER_OF_TASKS)
        .HorizontalAlignment = xlCenter
        .NumberFormatLocal = "0%"
        .FormatConditions.AddDatabar
        .FormatConditions(.FormatConditions.Count).ShowValue = True
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1)
            .MinPoint.Modify newtype:=xlConditionValueNumber, newvalue:=0
            .MaxPoint.Modify newtype:=xlConditionValueNumber, newvalue:=1
        End With
        With .FormatConditions(1).BarColor
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0.6
        End With
        .FormatConditions(1).BarFillType = xlDataBarFillSolid
        .FormatConditions(1).Direction = xlContext
        .FormatConditions(1).NegativeBarFormat.ColorType = xlDataBarColor
        .FormatConditions(1).BarBorder.Type = xlDataBarBorderNone
        .FormatConditions(1).AxisPosition = xlDataBarAxisAutomatic
        With .FormatConditions(1).AxisColor
            .Color = 0
            .TintAndShade = 0
        End With
        With .FormatConditions(1).NegativeBarFormat.Color
            .Color = 255
            .TintAndShade = 0
        End With
    End With
    
    'Highlight Today
    With ganttRange.Offset(-2, 0).Resize(ganttRange.Rows.Count + 2)
        .FormatConditions.Add Type:=xlExpression, Formula1:="=J$6=TODAY()"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Borders(xlLeft)
            .LineStyle = xlContinuous
            .Color = -16776961
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .FormatConditions(1).Borders(xlRight)
            .LineStyle = xlContinuous
            .Color = -16776961
            .TintAndShade = 0
            .Weight = xlThin
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    'Scroll Bars
    sh.ScrollBars.Add(weekCursor.Left, weekCursor.Top - 16.5, weekCursor.Resize(1, WEEK * 5).Width, 14).Select
    With Selection
        .Value = 1
        .Min = 1
        .Max = 52
        .SmallChange = 1
        .LargeChange = 10
        .LinkedCell = "R_DisplayWeek"
        .Display3DShading = False
    End With

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"
    
    sh.Range("I8").AutoFill sh.Range("I8").Resize(NUMBER_OF_TASKS)
    
    'Status Format
    With sh.Range("I8").Resize(NUMBER_OF_TASKS)
        .HorizontalAlignment = xlCenter
        .FormatConditions.Add Type:=xlTextString, String:="Completed", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbGray
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbGainsboro
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="In Progress", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbDarkGreen
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbHoneydew
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="Delay", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbSienna
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbBisque
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="Over Due", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbFireBrick
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbPink
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    
        .FormatConditions.Add Type:=xlTextString, String:="Not Started", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbDarkGray
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbWhite
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    'Format Start End Dates
    With sh.Range("D7:E7").Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent3
        .TintAndShade = -0.25
        .PatternTintAndShade = 0
    End With
    
    With sh.Range("F7:G7").Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent4
        .TintAndShade = -0.25
        .PatternTintAndShade = 0
    End With

    sh.Range("D6").Value = "PLANNED"
    sh.Range("D6:E6").Merge
    With sh.Range("D6:E6").Font
        .ThemeColor = xlThemeColorAccent3
        .TintAndShade = -0.25
    End With
    
    sh.Range("F6").Value = "ACTUAL"
    sh.Range("F6:G6").Merge
    With sh.Range("F6:G6").Font
        .ThemeColor = xlThemeColorAccent4
        .TintAndShade = -0.25
    End With
    
    With sh.Range("D7:E7,F7:G7").Borders(xlEdgeLeft)
        .LineStyle = xlContinuous
        .ThemeColor = 1
        .TintAndShade = 0
        .Weight = xlThin
    End With
    With sh.Range("D7:E7,F7:G7").Borders(xlEdgeRight)
        .LineStyle = xlContinuous
        .ThemeColor = 1
        .TintAndShade = 0
        .Weight = xlThin
    End With

    'Current Task Picker
    With sh.Range("A8").Resize(NUMBER_OF_TASKS)
        .Interior.Color = rgbWhiteSmoke
        .FormulaR1C1 = "=IF(AND(NOT(ISBLANK(RC[3])),RC[7]<1,RC[3]<=TODAY()),""▲"","""")"
        .HorizontalAlignment = xlRight
        .VerticalAlignment = xlCenter
        .Orientation = -90
        With .Font
            .Size = 11
            .Color = 192
        End With
    End With

    'Phase Format
    With sh.Range("B8").Resize(NUMBER_OF_TASKS)
        .FormatConditions.Add Type:=xlTextString, String:="Phase", TextOperator:=xlBeginsWith
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Bold = True
            .Italic = False
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    sh.Range("D8:G8").Resize(NUMBER_OF_TASKS).NumberFormatLocal = "yyyy/m/d;@"
    
    sh.Columns("A:A").ColumnWidth = 3
    sh.Columns("B:B").ColumnWidth = 35
    sh.Columns("C:C").ColumnWidth = 13
    sh.Columns("H:H").ColumnWidth = 10
    sh.Columns("I:I").ColumnWidth = 10
    
    'Holiday Format
    With sh.Range("J8").Resize(NUMBER_OF_TASKS, WEEK * 5)
        .FormatConditions.Add Type:=xlExpression, Formula1:="=OR(J$7=""S"",NOT(ISNA(VLOOKUP(J$6,$AT:$AT,1,FALSE))))"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Interior
            .Pattern = xlLightDown
            .PatternColor = 11711154
            .PatternTintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    With sh.Range("AT7")
        .Value = "Holidays"
        With .Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorLight1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
        End With
        With .Font
            .ThemeColor = xlThemeColorDark1
            .TintAndShade = 0
        End With
    End With

    With sh.Range("AT8").Resize(NUMBER_OF_TASKS)
        .NumberFormatLocal = "yyyy/m/d;@"
        With .Interior
            .Pattern = xlSolid
            .PatternColorIndex = xlAutomatic
            .Color = 13434879
            .TintAndShade = 0
            .PatternTintAndShade = 0
        End With
    End With
End Sub

在宅勤務用にミュートスイッチを作成

在宅勤務で受電時に瞬時にミュートできるスイッチを作ったので紹介しようと思う。

実際のモノはこんな感じで、だいたいマウスくらいのサイズ。ミュート中はLEDが赤く点滅する。
f:id:t-hom:20200606041804g:plain

なんでこんなものを作ったのか

私の自宅では定刻になったらラズパイが可愛らしい音声で食事や家事や運動や勉強を促してくれるガイダンスシステムを導入している。

それによってあらかじめ設計した生活リズムをキープしているのだが、在宅勤務だと残業で電話会議している最中に音声ガイダンスが発動するのでそれを黙らせるためだ。
黙らせるだけならラズパイ側でミュートしておけば良いんだけど、問題はミュートしたことを忘れて音声ガイダンスがずっと無効化されてしまうので、ミュート中はミュートであることをアピールするようなスイッチが欲しかった。

以上が作成の経緯。
まぁ他にも資料作成中なんかは音楽をかけたほうが仕事がはかどったりするので、その時に受電したら瞬時にミュートするという用途でも使っている。

構造

日本開閉器という会社の4極単投スイッチ(型番 S-41-J)を使用している。
これはON時に4回路が同時につながり、OFF時に4回路が切れるタイプ。

以下がデータシート
f:id:t-hom:20200606043445p:plain

1-3, 7-9, 4-6, 10-12という表記はどの接点が繋がるかを表し、これを図示するとこうなる。
f:id:t-hom:20200606050108p:plain

ちなみに2, 8, 5, 11の接点はこのスイッチにはなく、同シリーズのS-42-Jという型番用である。実はそっちを買えばもっとシンプルにできたことを知って結局買いなおしたのだが、まだ届いてないのでとりあえずS-41-Jで作った。

接続図

f:id:t-hom:20200606045346p:plain

3.5ミリ ステレオ3極ジャックのケーブルの中身はL線とR線とグランド線になってるので、ミュートの仕組み自体は単純に音声ケーブルの各端子をスイッチにつないでるだけのシンプルなもの。

以下のサイトを参考にしたんだけど、接続をよく見てなくて、単純にON・OFFだと思いこんでた。
実際にはL線とR線をグランド接続することでミュートしてるようだけど、どうするのが正解かよく分からないのでまぁ単純OFFで良いかなと。。
craftsman.gtfm.org


NOT回路の方は電気知識のない私にはややこしかったけど、B-1 論理回路の基本というサイトで予習しつつ、回路シミュレータ―でLEDに流れる電流が適切になるように抵抗値を調整して作成した。

makezine.jp

f:id:t-hom:20200606052327g:plain

中身

実は閲覧禁止レベルのひどい中身。試作機ということでご容赦を。。
f:id:t-hom:20200606053518p:plain

音声ケーブルはスイッチの端子穴に通してねじってるだけ、NOT回路とスイッチの接続はワニ口クリップからブレッドボードへ、USB給電からNOT回路へもワニ口クリップからブレッドボードへ、LEDはジャンプワイヤーのメスに挿してるだけで蓋を開けるたびに抜ける始末。

S-42-Jを利用した改善案

S-42-Jは、日本開閉器の4極双投スイッチである。最初データシートだけ見て意味が分かってなかったので変な風に接続されるのを嫌ってON-OFFスイッチのS-41-Jを買ったんだけど、S-42-Jを使えばNOT回路がそもそも要らないことに気づいた。
f:id:t-hom:20200606055133p:plain

図にするとこんな感じ。
f:id:t-hom:20200606055039p:plain

箱の中身はブレッドボードが丸ごと消えて、抵抗1個に置き換わるイメージ。

あとは現状の回路だとLEDがOFF時も電力を食うけど、NOT回路をなくして単純スイッチにすればLEDがOFFの間は電力が消費されないのでUSBから給電しなくても乾電池で1か月くらい実用的に動作するかもしれない。ミュートにするのは週に4時間の定例ミーティングと不定期の会議が毎日1~2時間くらい。このあたりもちゃんと計算して改善したい。

古いお風呂の入浴環境をUpgradeするアイテム

最近湯舟につかろうと思い立った。
これまで10年ほどほとんどシャワーのみで過ごしてきたのに、なんでまたこれから暑くなる今から入浴なのかというと、とある動画の影響である。
このところ疲れを感じていた私に、しっかり疲れを取るには入浴が効果的という文句が刺さった。

うちの風呂は設備が古く、湯舟もかなり小さい。なんというか、正方形で深いやつ。
肩まで浸かろうと思うとターミネーターの登場シーンばりに体を折り曲げなければならない。

それでもなんとか、疲労回復とリラックスタイムを手に入れたく、入浴環境をUpgradeした。

まず風呂蓋。そもそも風呂蓋がない。賃貸契約したときから付いてなかったから通うなら自分で買えということだろう。
それでこれを発注した。

70×79。うーん狭い。
これは届いているものの、まだ使っていない。
月曜にダスキンさんを呼んでるので、本格的に風呂掃除してもらってから、使おうと思う。

次にお湯張り用のタイマー

オリエント 湯温計付 バスアラーム 1003

オリエント 湯温計付 バスアラーム 1003

  • メディア: ホーム&キッチン

水位センサーと温度センサーがついていて、アラームで知らせてくれる。Amazonレビューでアラームが小さくて気付かないという書き込みがあったけど、私の場合はそんなこともなかった。多分部屋と風呂の距離と構造によるんだろうな。
最近のお風呂ならこんなもの無くてもお湯張り・温度調節もフルオートなんだろうけど、うちの風呂はアナログなので水の溜まり具合を逐一確認しなければあふれてしまうし、湯加減を時折チェックしなければ煮えたぎってしまう。

次に湯かき棒

風呂を混ぜるのに専用の道具があるというのは初めて知った。
うちは2穴の追い焚きタイプの風呂釜。いわゆる昔のやつ。水面を触ると火傷しそうなほど熱いのに水中は冷たいままで、手でかき混ぜるのも一苦労だったけどこれ超便利。ヒノキの良い香りがする。

最後に完全防水のBluetoothスピーカー

最新版があるらしいけど、私は気づかずに上記の商品を買った。

音質はやっぱ普段使いのBOSEが良すぎていまいちかなと思ったんだけど、風呂で聴くと反響のせいか結構良い感じに聞こえる。

個人的にはスピーカーが一番買って正解だったと思う。
私の場合、湯舟に使ってるとなんか手持無沙汰ですぐ上がってしまうので音楽があったほうがゆったりとリラックスできる。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。