memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

勤怠管理サービス「ジョブカン」のオリジナルコントローラをバージョンアップした

日常的な苦痛を少しでもITで緩和したい(解決するにはかなり大変なのでそこまでしなくても自分が使うだけを考えれば少しの知識と努力で何とかなるという信念なので「解決」とはかかず「緩和」としています←言い訳がましい)、以下のコントローラ?を作成して使ってきました。

memorandums.hatenablog.com

上記案には以下に示すかなり致命的な問題がありました。

  • ジョブカンはAPIを一般には提供していないためあくまでブラウザを操作するだけです。そのブラウザを操作するためにChrome拡張でWebシリアルでコントローラ側(Arduino)から要求があったらジョブカンのWebページ内のボタンをJavascriptで押下するようにしています。
  • Arduinoでボタン押下を検知したときにシリアル通信でPCに特定の文字を送るところまではキッチリ動作するのですが、現状案ではボタンを押したら、ジョブカンでその通信を受信してもしなくても状態が変わったものとして、Arduino側で音を鳴らしたりボタンを点滅させていました。
  • そのため、Arduinoで管理している状態と、ジョブカン側の状態が色々な原因でずれてしまうことたまに起きたんですね。。。出勤してボタンを忘れずに押したとして、ジョブカンで認識していなければずーっと「退勤中」になってしまいます。あーめんど。どげんかしたい。。。と思っていたのですが、面倒なのでなんとか「運用でカバー」(←便利な言葉です)してきました。

本日、思い立って何とかしてみたんです。はい。一応、成功しました。以下、Chrome拡張機能Arduinoのコードの説明になります。

Chrome拡張機能

ファイル一覧は以下の通りです。

  • manifest.json
  • background.js
  • a.js
  • icon128.png
  • icon48.png
  • icon16.png
  • on.mp3
  • off.mp3

以下、主要ファイルです。V3になって色々と変わったようです。ネットの記事を参考に作成しています。

manifest.json

{
  "name": "arduino-jobcan-controller",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "arduino-jobcan-controller",
  "permissions": ["activeTab",
  "scripting"],
  "web_accessible_resources": [{
    "resources" : [ "on.mp3", "off.mp3" ],
    "matches" : [ "*://*.jobcan.jp/*" ]
  }],
  "icons": {
    "16":"icon16.png",
    "48":"icon48.png",
    "128":"icon128.png"
  },
  "action": {
    "default_icon": "icon16.png"
  },
  "background": {
    "service_worker": "background.js"
  }
}

background.js 本体読み込み用

chrome.action.onClicked.addListener(async (tab) => {
   await chrome.scripting.executeScript({
     target: {tabId: tab.id},
     files: ['a.js']
   });
 });

a.js コード本体

  • Arduino側からシリアル受信した文字列の先頭が'I'だったらジョブカンのページ上のボタンを押下します。
  • その後、ジョブカンのバックエンド側で入退室状態を切り替える(切り替えない)を判断してジョブカンのページ上に「勤務中」かそれ以外の文字列を表示しますので、その状態が変わるであろう2秒後に、HTML要素をチェックして、もし「勤務中」だったらArduinoの'I'を、それ以外だったら’O’を送信するようにします。
  • 出退勤に切り替わった状態をみて、mp3ファイルで状態が切り替わったことを示す音を鳴らします。これ結構大変でした。。。audio要素をdocumentの子要素に追加しておけばいいようですね。やったことなかったので勉強になりました。
async function handleArduino() {
    const port = await navigator.serial.requestPort();

    const audio_on = new Audio();
    const audio_on_url = chrome.runtime.getURL("on.mp3");
    audio_on.src = audio_on_url;
    audio_on.load();
    const audio_off = new Audio();
    const audio_off_url = chrome.runtime.getURL("off.mp3");
    audio_off.src = audio_off_url;
    audio_off.load();
     
    async function writeText(text) {
        const encoder = new TextEncoder();
        const writer = port.writable.getWriter();
        await writer.write(encoder.encode(text));
        writer.releaseLock();
    }

    await port.open({ baudRate: 9600 });

    alert('Listening to Serial.port...');

    while (port.readable) {
        const reader = port.readable.getReader();
    
        try {
            while (true) {
                const { value, done } = await reader.read();
                if (done) {
                    reader.releaseLock();
                    break;
                }
                if (value) {
                    if (value[0] == 73) { //'I'
                        const btn = document.querySelector("#adit-button-push"); 
                        btn.click();

                        setTimeout(function() {
                            const status = document.querySelector("#working_status").getInnerHTML();
                            if (status == '勤務中') {
                                writeText('I');
                                audio_on.play();
                            } else {
                                writeText('O');
                                audio_off.play();
                            }
                        }, 2000);
                    } else {
                        console.log("???");
                    }
                }
            }
        } catch (error) {
        // TODO: Handle non-fatal read error.
        }
    }
}

handleArduino();

Arduino

  • 状態管理はすべてジョブカン側に任せているのでArduino側はあくまでスイッチのみになります。もし、ジョブカン側で入退室の状態が変わっていたら、シリアルで状態を一度だけ受信して、そのときにランプの点滅/消灯状態を切り替えます。
bool blinking = false;
int lamp = LOW;
long pre_time = 0;
bool send_flag = false;

void setup() {
  Serial.begin(9600);
  pinMode(2, OUTPUT);
  pinMode(3, INPUT_PULLUP);
  delay(500); //すぐ実行するとスイッチが押されたことになる
}

void loop() {
  if (digitalRead(3) == LOW) {
    if (send_flag == false) { 
      Serial.println("I");
      send_flag = true;
    }
  } else {
    send_flag = false;
  }

  if (Serial.available() > 0) {
    int v = Serial.read();
    if (v == 'O') {
      lamp = LOW;
      blinking = false;
    } else {
      blinking = true;
    }
  }

  if (blinking) {
    if (millis() - pre_time > 300) {
      lamp = !lamp;
      pre_time = millis();
    }
  }
  digitalWrite(2, lamp);

  delay(100);
}

これでボタンを押してもジョブカンにイベントが送信されたかどうかわからなくて嫌だ。。。という問題は解消されました。あとは僕が押し忘れなければいいわけです。

めでたしめでたし。