2011/02/13

ポケコンとMacをシリアルケーブルで繋ぐ

先日、知人と昔の趣味の話ついでに高校の頃から使っているポケコン(SHARP PC-1360K)の話をした。その後、無性にいじりたくなったので、会社の机に入れていたのを持ち帰って手始めにシリアルケーブルで接続してみた。このポケコンは今でもちゃんと使える。計算が非常に遅いし、他のポケコンのように関数キーが充実していないので使う事は無いけど。

さて我が愛機 PC-1360K には本体右手にシリアル入出力ポート(SIO)が付いている。15ピンで1.27mmピッチのようなので、まずは手持ちのパーツから自作コネクタを作成した。

丁度1.27mm->2.54mmピッチ変換基板を持っているのでピーカッターを使ってまっ二つに。










ピンヘッダからピンを抜く。











抜いたピンを PC-1360K の SIO に刺す。SIO は 15ピンなのだが、ピッチ変換基板は 14ピン。SIO 1ピンはフレームグランドなので、無視。グランドは信号グランドにてとるから良し。この時点で片面に予備半田を薄く載せておく。今回使用した基板は予備半田が載っていたので基板は予備半田してない。





位置を合わせたら、一本一本コテを当てて半田をしていく。














ヘッダピンを付ける。まだやってないが、1.27mm ピッチのピンは半田でかろうじて付いている状況。エポキシでも盛って強化しておきたい。実は半田が甘くて、実験中ピンが一本外れた。。ここでフロー制御無視のため 4ピン(RTS)と5ピン(CTS)をショートしてしまう。










実験用にRS-232C のメスコネクタとレベルシフタ(秋月で売ってる SP3232)を載せた万能基板を持っているので(RTS, CTS は基板上でショート)、電源, GND, TxD, RxD を IC クリップで接続すれば出来上がり。会社同僚のアドバイスの通り PC-1360K の TxD, RxD は極性が反転しているようなので、パーツボックスの中に見つけた 2SK1336 (NMOS) で簡易インバーターを作る(S->GND, G->SIO TxD, D->レベルシフタの TxD 入力、手持ちの10KΩを D、電源間を接続)。RxD はパーツ不足で保留(後で74HC04を見つけたが、DIP じゃないのでやる気がおきません)。






USB シリアルケーブルは Arvel の SRC06-USB。ドライバは FDTI 。FDTI のホームページには Mac のドライバもあるので、インストールしておく。うちでは /dev/cu.usbserial-FTDC6A59 で認識されている。

Mac (MacBook) 側はターミナルを開いて、標準付属の screen コマンドで、
% screen /dev/cu.usbserial-FTDC6A59 1200
を実行して受信準備。ポケコン側は、
> OPEN
> LPRINT "Hello World!"
と実行。結果が写真の通り。Mac のターミナルに表示されています♪





ちなみに PC-1360K のシリアル通信の最高速度は 1200bps 。これをデフォルトのパリティ無し、データ8ビット、ストップビット1の設定で実行してるのだけど、見てるとすごーく遅い。

余談。
ポケコン側の4ピン(RTS)と5ピン(CTS)をショートするのを忘れて、一日無駄にした。オシロが無いので動いているかどうかを判断するのが難しい。もちろんテスターはあるが。ポケコンから LPRINT を実行すると無反応になるので、その時点で気づくべきだった。またポケコンのボタン電池が弱っていたので、同じぐらいの電位を出力する USB ポートから直にポケコンに電源を与えていたのだけど、実験中にうっかりどこかにショートしたらしく、MacBook から 1A の過電流の警告を出してしまった。くわばらくわばら。
ちなみに SIO から受信するベーシックのコマンドが見当たらないのだが、気のせいか?レベルシフタを入れてから考えよう(INPUT#1でいいようだ。後で試そう)。

おしまい。

2011/02/05

Google ドキュメントを活用する (家計簿編3)


前回でほぼ完成した家計簿ですが、月ごとに自動的にデータを分けておきたいですよね。今回は月ごとにデータをまとめる関数を実装したいと思います。家計簿編はこれで完結です。簡単に実装できると思ったんですが、意外にはまりました。

実装したい内容は、フォームから入力されたデータを保存するシートをコピーして名前を変える、コピー元のシートの古いデータを消去、先月の残高を繰越金として入力する、だけなのですが、いざ繰越金を入力しようとすると、セルに値を書くことは出来るのですが、フォームから入力されたデータとして認識してくれません。つまり、フォームからデータを入力すると折角書いた繰越金が上書きされて消えてしまいます。フォームを管理するスプレッドシートを開くと、メニューバーのフォームの隣りに入力されたデータ数がカッコに囲まれて表示されています。フォームからデータを入力するとインクリメントされますが、普通にスプレッドシートにデータを setValue() で書き込んでもここは変化しません。
データ数をコントロールする API が分かればよいのでしょうが、そんなのは見つかるはずも無いだろうということで、フォームにスクリプトから入力する方法を模索してそれを実装しました。

使った API は UrlFetchApp.fetch(url, optAdvancedArgs) です。

引数の url と optAdvancedArgs はフォームのソースコードから見つけます。HTML でフォームを作ったことがある方なら分かると思いますが、
< form action="~~~" >
で示される URL です。ここで注意すべき点は、url に含まれる formResponse?formkey= の部分で、formResponse?hl=ja&formkey= なっている場合があります。hl=ja&は消しましょう。理由はよくわかりませんが、これを消さないとうまく動作しません。
また optAdvancedArgs を使った繰越金(収入欄)の指定は、< input type="text" name="entry.1.single" でも分かるように、entry.x.single で示されるネームへの引数指定になります。その他の変数も併せて、適宜個々の環境に合わせて設定して下さい。

では前回のスクリプトの代わりに以下の完成版のスクリプトをコピペして置き換えて下さい。解説は特に書きませんので、コメントを参考に理解して下さい。

//-------------------------------------------------------------------
// 家計簿スクリプト for Google docs
// Date: 2/4/'11
// Author: Tatsuro
//-------------------------------------------------------------------

// 定数定義
var UPDATE_DATE = 1;  // バックアップを実行したい日
var BALANCE_COL = 2;  // 残高の列番号B
var IN_COL      = 3;  // 収入の列番号C
var EX_COL      = 4;  // 支出の列番号D
var IN_COL_SUM  = 7;  // 収入合計の列番号G
var EX_COL_SUM  = 8;  // 支出合計の列番号H

// FORM の情報
var FORM_URL    = 'http://spreadsheets.google.com/formResponse?formkey=dGg2dnoyN3pKaEJtb2FfMFF5R3JmNEE6MQ&ifq';
var FORM_TAG_IN = 'entry.1.single=';  // 収入
var FORM_TAG_C1 = '&entry.3.single='; // 適用
var FORM_TAG_C2 = '&entry.4.single='; // 費目
    
//-------------------------------------------------------------------
// accessForm(value_form):
// 新規シートのセットアップのために繰越金(value_form)をフォームを経由して書き込む
//-------------------------------------------------------------------
function accessForm(value_form) {
  // Form に入力したい項目を設定する
  var post_str = 
      FORM_TAG_IN+encodeURIComponent(value_form)+
      FORM_TAG_C1+encodeURIComponent("先月")+
      FORM_TAG_C2+encodeURIComponent("繰越");
  var options = {
    "method"  : "post",
    "payload" : post_str
  };

  // Form にデータを POST する  
  UrlFetchApp.fetch(FORM_URL, options);
}

//-------------------------------------------------------------------
// backupLatest():
// 毎月一度の集計時に呼ばれることを前提とした関数
// 先月分をシートごとコピー、シート名変更、フォームのデータが保存されるシートの初期化を行う
//-------------------------------------------------------------------
function backupLatest() {
  // 今日の日付を取得する
  var today = new Date();

  // 日付がバックアップを実行したい日のみ実行
  if (today.getDate() == UPDATE_DATE) {
    // 左端のシートをアクティブにし、アクティブなシートの複製をつくる
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    ss.setActiveSheet(ss.getSheets()[0]);
    ss.duplicateActiveSheet();
    
    // 複製されたシート(左から2番目)をアクティブにする
    ss.setActiveSheet(ss.getSheets()[1]);
    
    // 複製されたシートのセル A2 のデータを取り出す(日付)
    var date_str = ss.getSheets()[1].getRange(2,1).getValue();
    // 複製されたシートのセル B1 のデータを取り出す(残高)
    var balance = ss.getSheets()[1].getRange(1,BALANCE_COL).getValue();
    
    // 複製されたシートの日付のデータから Date オブジェクトを生成し、年、月を抽出する
    // 文字列解析がめんどうなので(いや、正しい対処法か ww)
    var date = new Date(date_str);
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    
    // 複製されたシート名を年-月に変更する
    // (既に存在するシート名ではエラーになるので注意)
    ss.renameActiveSheet(year+'-'+month);

    // フォーム入力用のシート(左端のシート)を初期化する
    // 左端のシートのオブジェクト取得
    var s_new = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
    // 左端のシートでデータが書かれている最大の行数を取得する
    var maxRow = s_new.getLastRow();
    // 先月のデータを消す
    s_new.deleteRows(2, (maxRow-1));

    // 繰り越し入力
    accessForm(balance);
  }
}

//-------------------------------------------------------------------
// updateCalculation():
// フォーム入力後に呼ばれることを前提とした関数
// 残高の計算式を埋め込みアップデートする
//-------------------------------------------------------------------
function updateCalculation() {
  // 左端のシート(今月の家計簿・フォームのデータ保存先)のオブジェクトを取得する
  var s_latest = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];

  // 左端のシートでデータが書かれている最大の行数を取得する
  var maxRow = s_latest.getLastRow();

  // 支出の合計を計算したいので、その範囲を A1 形式の文字列にして準備する
  // 範囲は、支出列の入力したばかりの行と前回の支出合計欄
  var sumRngOutC = s_latest.getRange(maxRow,  EX_COL    ).getA1Notation();
  var sumRngOutL = s_latest.getRange(maxRow-1,EX_COL_SUM).getA1Notation();

  // 支出の縦合計の計算式を保管したいセルに先程の A1 形式を使っ埋め込む
  // 保管先のセルは入力したばかりの行の支出合計欄
  // 最初の行の場合は前回の結果が無いので今回の値を参照するだけ
  if (maxRow == 2) {
    s_latest.getRange(maxRow, EX_COL_SUM).setFormula('='+sumRngOutC);
  } else {
    s_latest.getRange(maxRow, EX_COL_SUM).setFormula('=SUM('+sumRngOutC+'+'+sumRngOutL+')');
  }
  
  // 収入の欄も同様に処理する
  var sumRngInC = s_latest.getRange(maxRow,  IN_COL    ).getA1Notation();
  var sumRngInL = s_latest.getRange(maxRow-1,IN_COL_SUM).getA1Notation();

  if (maxRow == 2) {
    s_latest.getRange(maxRow, IN_COL_SUM).setFormula('='+sumRngInC);
  } else {
    s_latest.getRange(maxRow, IN_COL_SUM).setFormula('=SUM('+sumRngInC+'+'+sumRngInL+')');
  }

  // 残高の計算のため、その元となる最後に入力した行の収入合計、支出合計、
  // 最後の行の残高の保管セルの位置を A1 形式の文字列で準備する
  var sumOut    = s_latest.getRange(maxRow, EX_COL_SUM).getA1Notation();
  var sumIn     = s_latest.getRange(maxRow, IN_COL_SUM ).getA1Notation();
  var refResult = s_latest.getRange(maxRow, BALANCE_COL ).getA1Notation();

  // 残高の計算式を埋め込む
  s_latest.getRange(maxRow, BALANCE_COL).setFormula('='+sumIn+'-'+sumOut);

  // 残高をフォームに表示する為、残高の計算結果の参照の式をセルにオーバーライド
  s_latest.getRange(1, BALANCE_COL).setFormula('='+refResult);
}
​

backupLatest() を単独で実行すると期待通りの動作を行います。上手く実行出来ない場合は、定数 UPDATE_DATE が実行したい日の指定に合っているかとどうか確認して下さい。UPDATE_DATE はスクリプトを実行したい日です。例えば日付が1日に変わった直後にバックアップをとれば丁度一月区切りで都合が良いと思います。

ところで、月に一度これを実行するにはどうすればよいかというと、前回使用したトリガーで、そのきっかけをフォームでなく、時間(Time-driven)で指定することで実現可能です。Time-driven は Minutes, Hour, Day そして Week をきっかけに起動できますが、Day timer の時間指定 (例えば夜中の0:00-1:00 の間)で自動でスクリプトを起動し、UPDATE_DATE と日付が合致した場合に限り残りの作業を行うようにします。
Time-driven トリガーを設定する前に必ずスクリプトのプロパティでタイムゾーンを合わせましょう。デフォルトでは日本になっていないようなので、注意です。また、タイムゾーンを設定する前に Time-driven トリガーを設定すると異なるタイムゾーンの設定が残ってしまいますので、その場合は一旦設定したトリガーを削除して、タイムゾーンを設定後にトリガーも登録しなおして下さい。

プロパティのメニュー




タイムゾーン設定

トリガー設定
以上でザックリですが、オートバックアップ付きの家計簿の例の紹介を終わります。まだ実験レベルでの提供による時間で起動できるトリガーも使えるというのは衝撃的事実でした。Google Apps Script にて Cron もどきの機能が使えるということは夢が広がりますね。