Eee note EA800 と Eee Note Sync の対話をキャプチャしてみた (2)

日本アジアカップ優勝おめでとう!

EA800 の解析事情

ここで外からちまちま TCP でつっついている間に、海外では EA800 を開腹して中の Micro SD を取り出すなどして解析が進められているらしい。

EA800のfirmwareが解析中 その1 | OSAKANA TAROのメモ帳

そのうち独自ファームウェアまで作られそうな勢いだ。熱い。

ファイルのダウンロード

あいにく手元に工具や SD カードリーダーが無いので、とりあえず外からつっつくのを続けてみたいと思う。色々試してみた結果、EA800 内のファイルをダウンロードする方法は概ね理解できた。大まかな流れとしては、xmlfile コマンドを使って EA800 内にあるファイルのリストを取得し、リストに載っている各ファイルを downloadfile コマンドでダウンロード、とすれば良さそうだ。

python スクリプト

そこで python で簡単なダウンローダを作ってみた。

2010-02-05 追記: フォルダIDに対応するパスが判明したのでソース先頭のコメント部に追記した

  #!/usr/bin/env python
  #-*- coding: utf-8 -*-
  
  #
  # ea800_downloader.py
  #
  #   Usage: python ea800_downloader.py FOLDER_ID OUT_DIR
  #   
  #   FOLDER_ID: ダウンロードしたいフォルダの ID (下記参照)
  #
  #    | ID | folder path                        |
  #    |----+------------------------------------|
  #    |  0 | /eTablet/var/ebook                 |
  #    |  2 | /eTablet/var/photos                |
  #    |  4 | /eTablet/var/enotes                |
  #    |  8 | ?                                  |
  #    | 12 | /eTablet/etc/db                    |
  #    | 14 | /eTablet/etc/template              |
  #    | 31 | /eTablet                           |
  #    | 32 | /usr/local/eTablet/bin             |
  #    | 33 | /eTablet/etc                       |
  #    | 34 | /eTablet/var                       |
  #    | 35 | /usr/local/eTablet/lib             |
  #    | 37 | /usr/local/eTablet/bin/ebookreader |
  #
  #   OUT_DIR: ダウンロードしたフォルダの格納先ディレクトリ
  #            OUT_DIR ディレクトリの直下に files と xmlfiles の 2 つのディレクトリを作り、
  #            files ディレクトリにはダウンロードしたファイルを格納し、
  #            xmlfiles ディレクトリにはファイルのリスト(XML形式)を格納する。
  
  
  import sys
  import os
  import socket
  import xml.etree.ElementTree as etree
  import hashlib
  
  
  BUF_SIZE = 8192
  
  
  class Connection(object):
      """
       Simple wrapper class for Raw TCP connection.
       Refer to: http://docs.python.org/howto/sockets.html
  
      Attributes:
       - _sock
      """
      
      def __init__(self, address):
          self._sock = socket.socket()
          self._sock.connect(address)
  
      def send(self, op):
          totalsent = 0
          op = op.encode("utf-8")
          op_size = len(op)
          while totalsent < op_size:
              sent = self._sock.send(op)
              if sent == 0:
                  raise RuntimeError, \
                      "socket connection broken"
              totalsent = totalsent + sent
  
      def recv(self, size=1):
          read = 0
          buf = []
          while read < size:
              data = self._sock.recv(BUF_SIZE)
              if data == "":
                  raise RuntimeError, \
                      "socket connection broken"
              read = read + len(data)
              buf.append(data)
              
          return "".join(buf)
          
      def close(self):
          self._sock.close()
  
  
  class Downloader(object):
      """
       File downloader for Eee Note EA800.
  
      Attributes:
       - _cs
       - _ds
       - _files_folder
       - _xmlfiles_folder
      """
      
      def __init__(self, out_dir):
          # connect to command socket
          self._cs = Connection(("169.254.2.1", 20000))
          # recieve welcome message
          self._cs.recv()
          
          # connect to data socket
          self._ds = Connection(("169.254.2.1", 20001))
          # recieve welcome message
          self._cs.recv()
  
          self._files_folder = os.path.join(out_dir, "files")
          self._xmlfiles_folder = os.path.join(out_dir, "xmlfiles")
  
      def close(self):
          # close both connections
          self._ds.close()
          self._cs.close()
  
      def _md5sum(self, data):
          m = hashlib.md5()
          m.update(data)
          return m.hexdigest()
  
      def downloadfile(self, folder_id, path):
          """execute 'downloadfile' command"""
          
          # send "downloadfile" request
          op = "downloadfile\n%d\nPath=%s\n\\n::\\n" % (folder_id, path)
          self._cs.send(op)
  
          # receive "downloadfilereturnvalue" response
          res = self._cs.recv()
          res_list = res.split(b"\n")
  
          assert res_list[0] == "downloadfilereturnvalue", "not correct response"
  
          # extract status and file_size from "downloadfilereturnvalue" response
          status = res_list[1]
          file_size = int(res_list[2])
          md5 = res_list[3][4:]  # drop first 4 character "md5="
  
          assert file_size > 0, "file_size is not positive integer"
  
          # prepare file and directory to output data
          outfile_path = os.path.join(self._files_folder,
                                      "%02d" % folder_id,
                                      path.lstrip("/"))
          outfile_dir = os.path.dirname(outfile_path)
          try:
              os.makedirs(outfile_dir)
          except OSError:
              pass
  
          # output data to local directory
          data = self._ds.recv(file_size)
          assert self._md5sum(data) == md5, "MD5 check sum invalid"
          outfile = open(outfile_path, "wb")
          outfile.write(data)
          outfile.close()
  
          print "downloaded: %s" % path
  
      def listfile(self, folder_id):
          """execute 'listfile' command"""
  
          # send "listfile" request
          op = "listfile\n%d\n\\n::\\n" % folder_id
          self._cs.send(op)
  
          # receive "listfilereturnvalue" response
          res = self._cs.recv()
          res_list = res.split(b"\n")
  
          assert res_list[0] == "listfilereturnvalue"
  
          # extract status and file_size from "listfilereturnvalue" response
          status = res_list[1]
          file_size = res_list[2]
          if file_size.endswith("\\n::\\n"):
              file_size = file_size[:-6]
          file_size = int(file_size)
  
          assert file_size > 0
  
          # output data to console
          data = self._ds.recv(file_size)
          print data
  
      def xmlfile(self, folder_id):
          """execute 'xmlfile' command"""
  
          # send "xmlfile" request
          op = "xmlfile\n%d\n\\n::\\n" % folder_id
          self._cs.send(op)
  
          # receive "xmlfilereturnvalue" response
          res = self._cs.recv()
          res_list = res.split(b"\n")
  
          assert res_list[0] == "xmlfilereturnvalue"
  
          # extract status and file_size from "xmlfilereturnvalue" response
          status = res_list[1]
          file_size = res_list[2]
          file_size = int(file_size)
  
          assert file_size > 0
  
          # prepare file and directory to output data
          outfile_path = os.path.join(self._xmlfiles_folder, "%02d.xml" % folder_id)
          try:
              os.makedirs(self._xmlfiles_folder)
          except OSError:
              pass
  
          # output data to local directory
          outfile = open(outfile_path, "wb")
          data = self._ds.recv(file_size)
          outfile.write(data)
          outfile.close()
  
          # parse xmlfile
          tree = etree.parse(outfile_path)
  
          return tree
              
      def downloadfolder(self, folder_id):
          """recursive download files in a folder"""
  
          tree = self.xmlfile(folder_id)
          self._traversal_and_download(tree.getroot(), u"", folder_id)
  
      def _traversal_and_download(self, elm, folder_name, folder_id):
          if elm.tag == "file":
              path = folder_name + u"/" + elm.get("name")
              try:
                  self.downloadfile(folder_id, path)
              except AssertionError, e:
                  print e
          else:
              for child in elm.getchildren():
                  self._traversal_and_download(child, elm.get("name", u""), folder_id)
  
  
  if __name__ == "__main__":
      folder_id = int(sys.argv[1])
      out_dir = sys.argv[2]
      
      d = Downloader(out_dir)
      d.downloadfolder(folder_id)
      d.close()
  

利用例

例えばコマンドプロンプト

python ea800_downloader.py 4 C:\tmp\eeenote

とすれば、フォルダID = 4 のフォルダ (/eTablet/etc/enotes) 内のファイルが C:\tmp\eeenote\files\12 内にダウンロードされます。

ただし自分用に適当に作ったものですので、何が起こっても責任は取れません。ファイルを削除してしまうようなことは無いと思いますが、ご利用は自己責任でお願いします。

次はアップロード

データのアップロードは、uploadfile と noteimportbook でインポートするか、restoreadd か restoredel を使ってリストアするか、のどちらかでいけそう。あと内蔵の evernote クライアントがうまく動いてくれないので、PC 経由で evernote 連携もできるようにしたい。