flymakeでrubocopを環境に合わせて実行

rubyを書く際にコーディングスタイルをrubocopでチェックしたい時、手動でチェックしていると忘れることがあるので違反があったらその場でエディタに警告して欲しいわけですが、これをemacsのflymakeでやる方法のメモ。

.emacsなどをいろんな環境で共有しているので、マシンによってrubocopが入っていたりなかったり、あってもグローバルでなく個々のbundleに入っていたりといった状況に対応できるようにします。(rubocopがない場合はruby -cによりシンタックスチェックのみ行います。)
(flymakeよりflycheckの方がいいのかもしれないですが、パッケージ等を追加で入れなくてもemacsについてくることを重視してflymakeにしてます。なんか時代錯誤 *1 )


まず、下記のshell scriptを ~/bin/ とか適当な場所に置きます。PATHが通ってなくても構いませんが、権限は実行可能にしておきます。

  • cat bin/flymake-ruby.sh
#!/bin/sh
RUBOCOP="rubocop --format emacs"

exists () {
    which "$1" >/dev/null 2>&1
}

# use rubocop directly
exists rubocop && exec $RUBOCOP "$@"

# use rubocop in the bundle
CWD="$PWD"
abspath="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
until [ "$PWD" = "/" ]; do
    if [ -f Gemfile.lock ]; then
        if grep rubocop Gemfile.lock >/dev/null; then
            exec bundle exec $RUBOCOP $abspath 2>&1
        fi
        break
    fi
    cd ..
done
cd "$CWD"

# if no rubocop is available, just use ruby syntax check
ruby -c "$@"


あとは .emacs に、flymakeでrubyスクリプトの編集中にflymake-ruby.shでチェックをかけて、違反があれば赤色表示になるように設定を追加します。以下では M-n / M-p キーでエラーのある位置に飛ぶとともに、エラー内容をminibufferに表示するようにしています。
(ruby-modeやflymake自体の設定などは適宜環境に合わせてください。なお以下ではバッククォートが \ (円マーク) になってしまっていますのでご注意を。)

;; ruby mode
(autoload 'ruby-mode "ruby-mode" nil t)
;(autoload 'ruby-mode "ruby-electric" nil t)
(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))

;; flymake
(require 'flymake)
(global-set-key "\C-cd" 'flymake-popup-current-error-menu)
(global-set-key "\M-n" 'flymake-goto-next-error)
(global-set-key "\M-p" 'flymake-goto-prev-error)
(defun display-error-message ()
  (message (get-char-property (point) 'help-echo)))
(defadvice flymake-goto-prev-error
    (after flymake-goto-prev-error-display-message) (display-error-message))
(defadvice flymake-goto-next-error
    (after flymake-goto-next-error-display-message) (display-error-message))
(ad-activate 'flymake-goto-prev-error 'flymake-goto-prev-error-display-message)
(ad-activate 'flymake-goto-next-error 'flymake-goto-next-error-display-message)

;; flymake for ruby
(defun flymake-ruby-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-file  (file-relative-name
                       temp-file
                       (file-name-directory buffer-file-name))))
    (list "~/bin/flymake-ruby.sh" (list local-file))))
(push '(".+\\.rb$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '(".+\\.rake$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("Rakefile$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("^\\(.*\\):\\([0-9]+\\): \\(.*\\)$" 1 2 nil 3) flymake-err-line-patterns)
(push '("^\\(.*\\):\\([0-9]+\\):[0-9]+: \\(.\\): \\(.*\\)$" 1 2 3 4) flymake-err-line-patterns)
(add-hook 'ruby-mode-hook
          '(lambda ()
             ;; Don't want flymake mode for ruby regions in rhtml files and also on read only files
             (if (and (not (null buffer-file-name)) (file-writable-p buffer-file-name))
                 (flymake-mode t))
             ))


rubocop自体の設定は RuboCopの設定アレコレ - Qiita などを参考に。

*1:そもそもemacsが…

コマンドの入出力が繋がった端末を移動させる

時間のかかるコマンドを起動した後、「あ、tmux(とかscreen)の中で実行すればよかった…」と気づくことがたまにあるわけですが、そんな時に強制的に端末を移動させる方法として、gdbをアタッチし、そのコマンドのファイルディスクリプタ(fd)の示す先を変更してしまうという手があります。


具体的な手順としては、open(2)で移動先の端末を開き、そのfdをdup2(2)の第1引数、移動前の端末に繋がっているfdを第2引数に指定して差し替え、最後にclose(2)で最初にopenした端末を閉じます。


ただし厳密には、対象が端末の場合、各種の状態を持っているので、これをsttyコマンドで合わせる必要があります。そうでないと、対象がviやemacsのような端末制御を行うソフトウェアの場合、正しく操作できなくなります。


またコマンドが入力を受け取る場合、移動先の端末でシェル等がreadしていると、入力が部分的にシェルに吸われてしまって操作が著しく困難になってしまうので、そのようなプロセスは止めておく必要があります。以下のスクリプトではSTOPシグナルを送って眠らせるようにしてあるのですが、screenやtmuxは、端末の子プロセスが寝たことを検知するとCONTシグナルを送ってすぐに起こしてしまうようです。仕方ないので手動でsleepでもさせておくしかなさそうです。。とりあえずスクリプトが実行された端末が移動先になっている場合にはスクリプト内でsleepさせています*1


このほかの工夫として、移動元の親プロセス(通常はシェル)を待機状態から復帰させるために、一旦対象コマンドにSTOPシグナルを送ってサスペンドしたのち、少し待ってCONTシグナルで復帰させています。ついでに、端末のサイズが変わったことをWINCHシグナルでコマンドに通知してあげます。これにより画面の再描画も必要に応じて行われます(あるいは単に無視されます)。


上記の処理をするのが本記事末尾のスクリプトです。

./switch-tty.sh 移動するブロセスID 移動先の端末またはファイル

のように指定して使います。


試してみるには、Linux上(またはssh)で端末を2つ開き、一方で何かのコマンド (例えば vi ) を起動し、もう一方で (必要ならtmuxなどを実行したのち)

./switch-tty.sh `pidof vi` `tty`

などと実行すると、

Current stdin: /dev/pts/5
Target fds: 0 1 2
session leader: 23965
Switching to this TTY
Continue? (y/N) 

と確認が出ますので、yを入力します。するとおもむろにgdbがごにょごにょ動いたのち、スクリプトを実行した端末にviが移動してきます。あとは、移動元の端末で disown を実行すれば、閉じてしまうことができます。


(なお、移動先でviを終了してもすぐにはシェルは帰ってきません。これはコマンドの終了を1分に1回しかチェックしていないためです*2。対象のコマンドの挙動次第ですが、 ctrl-C を入力すればsleepが中断されてすぐにシェルが戻ってくるかもしれません。)


以下、スクリプト
https://gist.github.com/NeoCat/f662cfd71c65eed7b59baed14eb3400c

*1:元々いたプロセスを終了してしまうわけにはいきません。セッションリーダーが閉じると端末そのものが閉じられてしまうからです

*2:自分の子プロセスでないプロセスの終了を待機するいい方法がないため

iPhoneのバッテリー残量を取得

iPhoneのバッテリーがかなりヘタってちょっと使うとすぐに電源が落ちてしまうようになってしまったので、自分で交換してみました。やり方を紹介したページを見つつ、バッテリー\2000 + 工具セット \1000 で無事交換成功し、日中そこそこ使っても丸一日持つようになりました。



DIGIFORCE 交換用PSEバッテリー iPhone6用 1810mAh LPB-DIGI6

DIGIFORCE 交換用PSEバッテリー iPhone6用 1810mAh LPB-DIGI6


さて、バッテリーの寿命を良くする方法として、80〜90%ほど充電されたら繋ぎっぱなしにせずに充電をやめると良いという説があるようです。真偽のほどは定かではありませんし、大きな効果があるならそういった機能が実装されているでしょうから、おそらくいちいち気にするほどの効果はないのでしょう。とはいえ、意識しなくとも自動的に充電が止まってくれるのならそうしてみても良いかな?という気になったので、やり方を考えてみました。


まず、iPhoneのバッテリー残量を取得する方法ですが、なんらかのアプリを使えないでしょうか? しかしiOSの場合、アプリがバックグラウンドで定期的に処理をするのは、アクセサリへのアクセスや、音楽再生や位置情報を使用するアプリに限られており、電池監視のためにはトリッキーなことをする必要があるので候補から外しました。


次に考えられるのはUSB越しで取得する方法です*1。最近のLinuxデスクトップ(Fedora 25で確認)にiPhoneをUSB接続すると、upowerコマンドでiPhoneのバッテリー値を取得できます。下記はiPadの場合ですが、まず一覧で名前を確認し、

$ upower -e
/org/freedesktop/UPower/devices/computer_3_1
/org/freedesktop/UPower/devices/mouse_0003o046Do1024x000A
/org/freedesktop/UPower/devices/keyboard_0003o046Do2011x000B
/org/freedesktop/UPower/devices/DisplayDevice

それっぽいデバイスを指定すると、

$ upower -i /org/freedesktop/UPower/devices/computer_3_1
  native-path:          /sys/devices/pci0000:00/0000:00:14.0/usb3/3-1
  vendor:               Apple_Inc.
  model:                iPad
  serial:               *****************************************
  power supply:         no
  updated:              20161231221652(14 seconds ago)
  has history:          yes
  has statistics:       no
  computer
    warning-level:       none
    percentage:          100%
    icon-name:          'battery-full-charged-symbolic'

という感じで情報が出力されるので、このpercentageを見るというのが一つの方法です。GUIで設定(gnome-control-center)→電源 で残量を確認できるのもこの情報を表示しています。


これを見て、80%を超えたらUSBの電源をOFFにする方法が取れるでしょう。LinuxからのUSBの電源制御には、最近だと以下のハブが使えるそうです。
LinuxからUSB HUBの電源のON/OFFを制御してみる - memoメモ


ただ欠点として、USBから切断してしまうとそれ以降の情報は取れないので、充電完了後に使用していると電池が減っていってしまうということになります*2


別のやり方として、iTunesをインストール済みのMacまたはWindowsであれば、libimobiledeviceを導入することで、Wi-Fi経由でも電池残量などの情報が取得できます。たまたまWiFiで電源をON/OFFできるコンセントを作ってあったので、全てWiFi越しでやれるということもあり、今回はこの方法を試してみました。


まず、libimobiledeviceを導入します。Macであればbrewで一発です。とはいえiOS10のせいか、HEADでないとうまく動作しませんでした。

$ brew install --HEAD libimobiledevice

これで、
idevice_id -l コマンドでUSBまたはWiFiで繋がっているiOSバイスのシリアルを調べておき、ideviceinfoでバッテリー情報を取得します。

$ ideviceinfo -u ****************(idevice_idで表示されたシリアル) -q com.apple.mobile.battery
BatteryCurrentCapacity: 100
BatteryIsCharging: false
ExternalChargeCapable: true
ExternalConnected: true
FullyCharged: false
GasGaugeCapability: true
HasBattery: true

BatteryCurrentCapacityが%単位でのバッテリー残量です。
rubyであれば

$IDEVICEINFO_CMD = '/usr/local/bin/ideviceinfo'

def get_batt(device_id)
  result = `#{$IDEVICEINFO_CMD} -u #{device_id} -q com.apple.mobile.battery`
  if result =~ /BatteryCurrentCapacity:\s*(\d+)/
    return $1.to_i
  end
  nil
end

というような関数でバッテリー残量を取得できるようになります。


ただし,iPhoneがスリープ状態になっている場合、WiFiには90〜300秒に一度、数秒間しか接続されないようで、この瞬間しか情報がとれません。とりあえず、2秒に一度くらいポーリングをかけることにします*3。また、それでもかなり長いことWiFiに接続してこないこともあるようなので、そのような場合には一度充電をOFF→ON(またはON→OFF)することで、iPhoneをウェイクアップさせれば情報がとれます*4


この結果を見て、80%以上で充電停止、80%未満で再開するようにします。また、充電中にMacがスリープしてしまうと充電されっぱなしになるので、充電中はcaffeinateコマンドでスリープを抑制しています。全体的にはこんなスクリプトになりました。


30%充電の状態からこのスクリプトを動かしながら充電を試して見た結果、こんな感じになりました。運悪く80%になるわずか手前でWiFi接続が切れたらしく、80%を3%ほど超えたところで充電が止まりました。



そのままiPhoneを使っていると、79〜80%で維持されるように充電がON/OFFされます。



でもこれ結局充放電してるわけで、バッテリーに優しい気はしない。。まあ、あくまで実験ということで。

*1:Bluetooth LEのBattery Serviceを使ってとれないか試したのですが、いざ取得しようとすると認証を要求されてしまい失敗しました。

*2:定期的に再接続して充電残量をチェックすればいいかも

*3:本当はlibimobiledeviceから接続イベントを取れれば良いのですが、ポーリングでも大した負荷ではないのて今回は適当に済ませてしまいました。

*4:この時、いちいち接続音やバイブレータが鳴りますが…

Amazon Dash Buttonは20分で照明のリモコンになった

遅ればせながら、Dash Buttonを受け取ったので、さくっと照明のリモコンにしました、という話。
すでにAmazon Dash Buttonのハックはやり尽くされている感がありますね。。


参考リンク: Amazon Dash ButtonをただのIoTボタンとして使う - Qiita


うちの場合、node.jsもインストール済み、照明のリモコンにするためのWeb APIも揃っていたので、あっという間にできましたとさ。


その時のツイート。リンク先に飛ぶと実行したコマンドとかがずらずら書いてあります。
Dash Buttonを照明のリモコンにしてみた


続き: その後Amazon Dash Buttonは「にゃーんボタン」になった - Okiraku Programming

その後Amazon Dash Buttonは「にゃーんボタン」になった

( Amazon Dash Buttonは20分で照明のリモコンになった - Okiraku Programming の続き )


Dash buttonで照明をつけられるようにしたものの、うちの照明はすでにSiriでも操作できるため物理ボタンは今さら必要ないと気づいたので、Dash buttonを「にゃーん」とTwitterに呟くボタンにすることにしました。



以下のサイトの"Get URL"ボタンを押し、Twitterのアカウントで認証すると、GETリクエストを送るだけで「にゃーん」などと呟く自分用のURLをゲットできますので、これをDasherのconfig.jsonのリクエスト先に設定します。


にゃーんボタン http://nyaan-button.appspot.com/

{"buttons":[
  {
    "name": "NYAAN",
    "address": "12:34:56:78:9a:bc",
    "url": "http://nyaan-button.appspot.com/update?token=XXXXXXXXXXXXXXXXXXXXXXXX",
    "method": "GET"
  }
]}


あとはDasherを再起動し,ボタンを押せば即座ににゃーんと呟けます。なおツイート内容の重複を避けるため、ランダムに猫の顔文字が付加されたり違うことを呟くこともありますので、あらかじめご了承ください。

SLコマンド on touch bar

新しいMacBook Proのtouch barで走るSLコマンドを作りました。
(touch barシュミレータでしか動作確認してませんが。)


https://raw.githubusercontent.com/NeoCat/sl_on_touchbar/master/sample.png



オリジナルのようにオプション(-a, -l, -F)には対応していません。


ソース(Swift)は以下。ビルド済みのバイナリも入ってます。
https://github.com/NeoCat/sl_on_touchbar


追記: 新しいmacOSでは、開発元が確認できないので開けない、というエラーが出ますが、一度 中にある「sl_on_touchbar」というappをctrlクリックして開くを選択すると、エラーダイアログに「開く」というボタンが現れますので、これをクリックすると以降は普通に開けるようになります。


なおビルドとシミュレータの動作にはmacOS Sierra 10.12.2 beta (16C41b)以降、XCode 8.2 beta以降が必要です。


slが走っている間はウィンドウのフォーカスが奪われるので、実質何も操作できなくなります。端末しか占拠しない普通のSLコマンド以上にうざいです。


これは、touch barに何か出すにはアプリケーションのウィンドウを手前に出している必要があるため。
普通のSLコマンドのような使用感を得るために、UIElement=YESにしてDockアイコンが出ないようにしたアプリケーションを起動してサイズ0×0のウィンドウをこっそり開くことで、touch barへの描画権を得ています。こいつが常に手前に出るせいでフォーカスが奪われるというわけです。

SL画像の差し替え [11/16追記]

「長い列車写真」の情報をコメントでいただいたので、touch barにこの写真を流せるようにしました。
slコマンドの引数か、環境変数 SL_IMAGE のいずれかに写真ファイル(高さを60ピクセルにしてください)へのパスを設定しておくと、そのファイルの画像が使われます。

参考: https://github.com/avatsaev/touchbar_nyancat

ESP8266でNAT(NAPT)でWi-Fiを中継する

ESP8266(ESP-WROOM-02)を最近よく使っています。

ESP8266を使ったセンサを、Wi-Fiアクセスポイントから遠い、直接電波の届きにくい場所に置きたかったため、ESP8266をもう一つ使ってWi-Fiの到達範囲を拡張する中継機にした(といってもブリッジではなくNAPTルータ)という話です。


とその前に軽くESP8266について紹介しておくと…(もう知ってる方は読み飛ばしてください)

ESP8266の紹介

ESP8266は無線LANモジュールです。購入した状態では外部のマイコンからATコマンドで制御できます。が、LX106という32bit MCUを搭載しており、このモジュール単体でArduino IDEからプログラミングして動作させることが可能です。この方が圧倒的に便利なので、私自身はATコマンドで使ったことはありません。
しかも安価(単体なら400円台)ですし、ESP-WROOM-02技適も取得していて安心して使え、AVRと比べて高性能(CPUクロック80〜160MHz, 4MBフラッシュ, 36KB RAM)。アナログ入力も1ch搭載していて、センサ・アクチュエータ等を簡単にネットワーク接続できるので非常に便利です。


Arduinoとして利用する方法は下記などが参考になります。
技適済み格安高性能Wi-FiモジュールESP8266をArduinoIDEを使ってIoT開発する為の環境準備を10分でやる方法 - Qiita


なお開発時はモジュール単体よりも、下記のようなブレークアウトボード・開発ボードを購入するのが良いでしょう。特にESPr DeveloperはUSBを接続するだけでいきなり開発が始められ、シリアル変換や書き込みモード設定など気にしなくて済むので非常に楽です(お値段はしますが)。
ESP-WROOM-02ピッチ変換済みモジュール《フル版》 - スイッチサイエンス
ESPr® Developer(ピンソケット実装済) - スイッチサイエンス


ちなみにシリアルでスケッチを書き込むと、ファームウェアが大きいため*1転送に少し時間がかかりますが、Wi-Fi経由で高速にアップロードすることも可能です。
ESP-WROOM-02 + ArduinoOTAでスケッチのWiFi経由アップロード - Qiita


他にもLua等でもプログラミングが可能なファームウェアもあります。
ESP8266(ESP-WROOM-02)自分的まとめ - 半空洞男女関係

Wi-Fiのモード

ESP8266は、自身がWi-FiのアクセスポイントになるWIFI_APモード、既設のWi-Fiアクセスポイントに接続するWIFI_STAモードの両方をサポートしています。
しかしこれ以外にWIFI_AP_STAという、他のアクセスポイントに接続しつつ、同時に自分もアクセスポイントとして他のクライアントからのWi-Fi接続を受け付けるということができます。(ちなみにArduino化した直後のデフォルトではこのモードになっているため、明示的にモード指定していないと ESP_xxxxxx というSSIDが発信されています。APのON/OFFやSSID等の設定はフラッシュに不揮発に保存され、前回の値が電源ONに設定されます。)


モードの切り替えは以下のようにWiFi.mode()を使用します。

const char *ssid = "ParentAP";
const char *password = "********";

const char *ap_ssid = "ESPap";
const char *ap_password = "ESPap_password";
...
  WiFi.mode(WIFI_AP_STA);  // モード設定

  WiFi.softAP(ap_ssid, ap_password);  // APのSSID・パスワード設定
  IPAddress myIP = WiFi.softAPIP();   // APとしてのIPアドレスを取得。デフォルトは 192.168.4.1 ?
  Serial.print("AP IP address: ");
  Serial.println(myIP);

  WiFi.begin(ssid, password);  // 別のAPに接続


子端末になるESP8266では、上記のap_ssidに対してWiFi.begin()で接続することになります。こうすると以下のような接続状態になります。

 親AP(192.168.10.1) <=> (192.168.10.x) ESP8266 (AP: 192.168.4.1) <=> (192.168.4.y)子端末

なお、親と子で別のサブネットのアドレスになっていないと(少なくとも親ネットワーク側からは)通信不能となるのでご注意を。


IPアドレスDHCPで自動的に設定されます(静的に指定もできます)。
また、ESP8266WiFiMultiを使って複数の接続先SSIDを指定しておけば、接続できる方に自動接続するということも可能です。詳しくはWiFiMultiというサンプルスケッチを見てください。

Wi-Fi中継の方法

親APとクライアントに同時接続できるということは、Wi-Fi中継機として使えるのでは?と思うわけですが、そのままでは中継機能(IP_FORWARD)は無効化されており、SDKに含まれるIPスタック(lwip)の設定を変更する必要があります。

参考: ESP8266 lwip IP_FORWARD/routing - ESP8266 Developer Zone


これにより、親APのネットワークとESP8266 APのネットワークの間でパケットが転送されるようになります。しかし、これだけでは親APやそのネットワーク内の各端末に「192.168.4.0/24 にアクセスするには 192.168.10.x をルータとして使う」というルーティング情報を設定しないと、通信が行えません(子端末からのパケットはESP8266がデフォルトゲートウェイになるので設定しなくとも親APのネットワークに出て行けますが、その応答パケットが戻っていくためのルーティング情報を設定しないと応答が得られません)。


この設定を不要にする方法として、普通のルータで使われている方式としてNAT(正確にはNAPT)があります。子端末から親ネットワークに出ていく際にソースアドレスをESP8266のもの(192.168.10.x)に書き換えておき、逆にこれに対する応答パケットが来た際には適切に子端末宛てに書き換えるというものです。(戻り先を特定するためにはセッション管理が必要になります。)


なお他にネットワークブリッジとして機能させることも考えられますが、こちらは色々ルーティングに改造を加えないと難しい気がして手を出していません。

NATを実装する

esp8266のSDKのlwipをざっと眺めたところ、NATの機能はないようでしたので、自分で実装してみました。


変更の内容はGitHubに置いてあります。
https://github.com/NeoCat/esp8266-Arduino/commit/4108c8dbced7769c75bcbb9ed880f1d3f178bcbe
上記のバグ修正コミット
https://github.com/NeoCat/esp8266-Arduino/commit/634dfb5f60e902c681c95712bc7455e24773667d


ボードマネージャ等でESP8266のライブラリを導入している場合は、以下のパッチを当てることで対応できます。
https://gist.github.com/NeoCat/da3c141813980edaa256ad351cab3a2c (バグ修正コミットを含む差分)


上記からRaw(またはDownload ZIP)でパッチを適当な場所に保存し、Macであれば端末から

$ cd ~/Library/Arduino15/packages/esp8266
$ patch -p1 < (保存した場所)/0001-Adds-NAPT-and-port-mapping-functionality-to-esp8266-.patch

とすればパッチを適用できます。適用後、lwipを再コンパイルする必要があります。

$ cd hardware/esp8266/2.3.0/tools
$ ln -s ../../../../tools/xtensa-lx106-elf-gcc/1.20.0-* xtensa-lx106-elf
$ cd sdk/lwip/src
$ make && make release  #=> intalled to ../../lib/liblwip_gcc.a


パッチを適用したら、Arduinoのスケッチ内で以下のようにすることでNATを有効化できます。

#define IP_PROTO_TCP     6
#define IP_PROTO_UDP     17
// SDKに追加した関数のプロトタイプ宣言
extern "C" {
  void ip_napt_enable(unsigned long addr, int enable);
  void ip_portmap_add(byte proto, unsigned long maddr, unsigned short mport,
                      unsigned long daddr, unsigned short dport);
  bool ip_portmap_remove(byte proto, unsigned short mport);
}
...
    // WiFi.begin等の後で
    ip_napt_enable(WiFi.softAPIP(), 1);

これ以降、子端末からのTCP/UDP/ICMP(pingのみ)のパケットは親ネットワークにアドレス変更の上で転送され、応答も対応付けてアドレス変換されます。
なおTCP, UDPの送信元ポート番号(1024以上)も重複を避けるために変換対象になります。


親ネットワークから子端末のTCP/UDPサーバにアクセスするためには、ESP8266の特定ポートを子端末にマッピングする必要があります。このためにはスケッチから、

        ip_portmap_add(IP_PROTO_TCP, WiFi.localIP(), 8080,
                                     IPAddress(192,168,4,3), 80);

のように、対象プロトコル(TCPまたはUDP)、ESP8266上のマッピングするポート、転送先となる子端末のIPアドレス、ポートを指定します。
この例では、親ネットワークから 192.168.10.x:8080 にアクセスすると、子端末192.168.4.3の80番ポート(Webサーバ)に接続できます。
ポートマッピングを解除するには、以下のようにします。

        ip_portmap_remove(IP_PROTO_TCP, 8080);

なお実装上、1つの宛先(アドレス+ポート)に対しては1つのポートしかマッピングできず、同一の宛先を複数ポートにマップすると動作がおかしくなるのでご注意ください。

制限事項

  • NATテーブルがメモリ(グローバル変数領域)を消費する。エントリ数は512となっていますが、これだと12KB消費します。子端末がせいぜい1,2セッションくらいしか張らないならこんなに要らないので、パッチ中の IP_NAPT_MAX を32等に小さくすれば、メモリを節約できます。(エントリはセッション切断後もタイムアウトするまでしばらく残るので、多めにしないと新たな接続ができなくなります。)逆にPC等をクライアントにしてしまうと、512でギリギリ足りるかどうかといったところです。
  • そんなに速度は出ません。単一TCPセッションのみで測ってみると1MB/s出たら良い方という感じです。センサーデータを送るくらいなら十分すぎますが、PCとかスマートフォンを接続するのには向かないでしょう。
  • TCPセッション管理がやや適当。TCPシーケンス番号を管理していない
  • ICMPパケットのping以外のメッセージに対応していない
  • 1つの宛先(アドレス+ポート)あたり1つのポートマッピングしかできない(NAPTエントリの数を節約するため)
  • AS-ISなので、どこか何かバグってるかも?(言い訳)

*1:ネットワークスタック等のファームウェアも含まれているため