0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

node.jsでEXEをAPI化(felicaのIDm取得アプリをAPI化)

Last updated at Posted at 2025-10-22

前回投稿の、FelicaのIDmを取得するEXEをAPI的に利用するために、node.jsで動くようにしてみました。

前提

node.jsnpmはインストール済み
PowerShell管理者実行

フォルダを準備

cd
mkdir idm-server
cd idm-server

準備

無難なexpressを使います。
コード変換はiconvで・・

npm init -y
npm install express iconv-lite

node.jsプログラム作成

notepad server.js

POSTで呼んで、結果は、JSONで出力する感じにしてみました。

実は、自分は、「メモ帳」がエディタな人なんですが、みなさまはなにかもっといいエディタを使っているのかな!?!?

// server.js
const express = require('express');
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs/promises');
const iconv = require('iconv-lite');

const app = express();
app.use(express.json({ limit: '32kb' }));

// ▼ 設定(環境に合わせて)
const EXE_PATH = path.resolve('./read_idm_felicalib.exe');
const EXEC_TIMEOUT_MS = 15000;          // 15秒でタイムアウト(用途で調整)
const MAX_BUFFER_BYTES = 1 * 1024 * 1024; // 出力上限 1MB

// ▼ 簡易ミューテックス(直列化)
let lock = Promise.resolve();
const withLock = (fn) => {
  const start = lock.then(() => fn());
  lock = start.catch(() => {}); // エラーでも次に進める
  return start;
};

// ▼ IDm抽出:16桁のHEXを拾う
function extractIDm(text) {
  // 16進数が8バイト(16桁)並んでいるパターン(スペースまたはコロン区切り)
  const re = /((?:[0-9A-Fa-f]{2}[\s:]){7}[0-9A-Fa-f]{2})|([0-9A-Fa-f]{16})/;
  const m = text.match(re);
  if (!m) return null;

  const hex = (m[1] || m[2]).replace(/[\s:]/g, '').toUpperCase();
  return hex.length === 16 ? hex : null;
}

// ▼ 実行関数(cp932→utf8、タイムアウト、上限付き)
async function runReader(args = []) {
  await fs.access(EXE_PATH); // 無ければ throw

  return new Promise((resolve, reject) => {
    const child = spawn(EXE_PATH, args, {
      cwd: path.dirname(EXE_PATH),
      windowsHide: true,
      shell: false,
      stdio: ['ignore', 'pipe', 'pipe']
    });

    let outBufs = [];
    let errBufs = [];
    let outSize = 0, errSize = 0;
    let finished = false;

    const timer = setTimeout(() => {
      if (!finished) {
        finished = true;
        child.kill('SIGKILL');
        resolve({ ok: false, timeout: true, code: null, stdout: '', stderr: '' });
      }
    }, EXEC_TIMEOUT_MS);

    const pushChunk = (bufs, sizeRef, chunk) => {
      const size = sizeRef + chunk.length;
      if (size <= MAX_BUFFER_BYTES) {
        bufs.push(chunk);
        return size;
      }
      return size; // 超過分は破棄
    };

    child.stdout.on('data', (chunk) => { outSize = pushChunk(outBufs, outSize, chunk); });
    child.stderr.on('data', (chunk) => { errSize = pushChunk(errBufs, errSize, chunk); });

    child.on('error', (err) => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      reject(err);
    });

    child.on('close', (code, signal) => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);

      // ▼ cp932としてデコード(日本語メッセージ想定)→ UTF-8文字列
      const stdout = iconv.decode(Buffer.concat(outBufs), 'cp932');
      const stderr = iconv.decode(Buffer.concat(errBufs), 'cp932');

      resolve({ ok: code === 0, code, signal, stdout, stderr });
    });
  });
}

// ▼ API:IDmを1回読む(直列化)
app.post('/read-idm', async (req, res) => {
  // 必要なら body にオプション(例: ポーリング系の引数)を受けてホワイトリスト
  const args = Array.isArray(req.body?.args) ? req.body.args : [];

  try {
    const result = await withLock(() => runReader(args));

    // タイムアウト
    if (result.timeout) {
      return res.status(504).json({ ok: false, error: 'timeout', message: 'タッチ待ちがタイムアウトしました' });
    }

    // IDm抽出
    const idm = extractIDm(`${result.stdout}\n${result.stderr}`) || null;

    // 成功扱いの条件:
    // - 終了コード0 かつ IDm が抽出できた
    if (result.ok && idm) {
      return res.json({ ok: true, idm, raw: { code: result.code, stdout: result.stdout, stderr: result.stderr } });
    }

    // ここに来たら失敗(終了コード≠0 or IDmなし)
    return res.status(500).json({
      ok: false,
      error: 'read_failed',
      idm,
      code: result.code,
      stdout: result.stdout,
      stderr: result.stderr
    });

  } catch (err) {
    // EXEが見つからない/起動できない等
    return res.status(500).json({ ok: false, error: 'spawn_error', message: String(err) });
  }
});

app.listen(3000, () => {
  console.log('FeliCa IDm API running on http://localhost:3000');
});
app.use(express.static('public'));

node.jsで実行

node server.js

これだと、powershell 上げっぱなしになるので、バックグラウンドで起動させるには、

forever start server.js

にしてください。

foreverが入っていなければ、

npm install -g forever

でインストールできます。

実行テスト

curl.exe -X POST http://localhost:3000/read-idm -H "Content-Type: application/json" -d "{}"

やったー。

{
  "ok": true,
  "idm": "XXXXXXXXXXXXXXXX",
  "raw": {
    "code": 0,
    "stdout": "XX XX XX XX XX XX XX XX\r\n",
    "stderr": ""
  }
}

webでも試したいです。

今、idm-serverフォルダにいる前提で、publicにndex.htmlを作成します。

mkdir public
notepad index.html

public¥ondex/html

<!doctype html>
<meta charset="utf-8">
<title>FeliCa IDm Test</title>
<style>body{font:16px/1.6 system-ui;margin:24px}button{font-size:16px;padding:8px 16px}</style>
<h1>FeliCa IDm 読み取り</h1>
<button id="btn">タッチして読み取り</button>
<pre id="out"></pre>
<script>
  const $btn = document.getElementById('btn');
  const $out = document.getElementById('out');
  $btn.onclick = async () => {
    $btn.disabled = true; $out.textContent = 'カードをタッチしてください…';
    try {
      const r = await fetch('/read-idm', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
      const j = await r.json();
      if (j.ok) {
        const colon = (j.idm.match(/.{2}/g)||[]).join(':');
        $out.textContent = `IDm: ${j.idm} (${colon})`;
      } else {
        $out.textContent = `失敗: ${j.error || 'unknown'}\ncode=${j.code}\nstdout=${j.stdout}\nstderr=${j.stderr}`;
      }
    } catch (e) {
      $out.textContent = '通信エラー: ' + e;
    } finally { $btn.disabled = false; }
  };
</script>

ブラウザでテスト

http://localhost:3000/

完成

めでたしめでたし。

問題は、このあと、どう使うかなんですけどね・・

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?