いっぺーちゃんの いろいろやってみよ~

micropython on ESP32 でLINE

Twitterに飽きたらLINEもね。ということで、Espruino on ESP32 で LINE(その1) のmicprpython版です。

準備として、上のリンクにある通り、アクセストークンを取得しておきます。

 

 以下がLINE Notifyにメッセージを送信するモジュールです。
tiny_line.py という名前で保存し、upipmなどでインストールしておきます。

import usocket as socket
import ussl as ssl

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ---- percent encoding
def RFC3986_encode(s):
    ret = ''
    bts = s.encode('utf-8')
    for c in bts :
        if c in range(0x30, 0x39 + 1) or \
           c in range(0x41, 0x5a + 1) or \
           c in range(0x61, 0x7a + 1) or \
           c in (0x2d, 0x2e, 0x5f, 0x7e):
            ret += chr(c)
        else :
            ret += '%%%02X' % (c)
    return ret

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class tiny_line :
    def __init__(self, access_token, debug=False) :
        # パラメータチェック
        if type(access_token) is not str:
            raise ValueError("access key must be string")
        
        self.access_token      = access_token
        self.__DEBUG__         = debug

    def __debug_print(self, str) :
        if self.__DEBUG__ :
            print(str)
    
    def __makeRequestMessage(self, method, host, message) :
        method = method.upper()                                     # to upper
        
        # make request
        request = method + ' ' + host['endpoint'] + ' HTTP/1.0\n'   # HTTP/1.0 を指定するとChunked Transferにならないので、レスポンスが1行で済む
        
        # make message body
        body = 'message=' + RFC3986_encode(message)
        
        # make message header
        header  = 'Host: ' + host['host'] + '\n'                        \
                + 'User-Agent: micropython line Bot v0.1\n'             \
                + 'Accept: */*\n'                                       \
                + 'Authorization: Bearer ' + self.access_token +'\n'    \
                + 'Content-Type: application/x-www-form-urlencoded\n'   \
                + 'Content-Length: ' + str(len(body)) + '\n'
                # HTTP/1.0の場合は以下は要らない
                #+ 'Connection: close\n'                                 \

        # request message
        ret = request + header + '\n' + body + '\n'
        return(ret)
    
    def __sendmessage(self, host, msg) :
        sock = socket.socket()
        addr = socket.getaddrinfo(host['host'], host['port'])[0][-1]
        
        # connect socket
        sock.connect(addr)
        try :
            # SSL wrap
            ssl_sock = ssl.wrap_socket(sock)
            
            # send data
            ssl_sock.write(msg)
            
            # 受信データの最初の1行
            rcv_line = ssl_sock.readline()
            protover, status, msg = rcv_line.split(None, 2)
            # self.__debug_print('%s::::%s::::%s' % (protover, status, msg))
            # status 200以外はエラー
            if status != b"200":
                raise ValueError(status)
            # それ以外のレスポンスヘッダを読む
            while True:
                rcv_line = ssl_sock.readline()
                # self.__debug_print(rcv_line)
                if not rcv_line:
                    # なんらかの異常なレスポンス(ヘッダが終わる前にデータがなくなった)
                    raise ValueError("Unexpected EOF in HTTP headers")
                if rcv_line == b'\r\n':
                    # 空行でヘッダ終了
                    break
        except Exception as e:
            # エラーが発生したらクローズして上位へ例外通知
            ssl_sock.close()
            raise e
        # メッセージ本体を受信(とりあえず読み捨て)
        # 制限回数を超えた場合なんかはここでチェックが必要?
        rcv_line = b''
        while True :
            try :
                l = ssl_sock.readline()
            except Exception as e:              # エラーが発生したらクローズして上位へ例外通知
                ssl_sock.close()
                raise e
           
            if not l:                           # データがない → 終了
                break
            
            # 読み込んだデータをためる
            rcv_line += l
        
        # self.__debug_print("@@@@" + str(rcv_line))
        self.__debug_print("close!!")
        ssl_sock.close()
    
    # ######## notify API ################################
    def notify(self, msg) : 
        host = { 'host': 'notify-api.line.me', 'port': 443, 'endpoint': '/api/notify'}
        
        reqMessage = self.__makeRequestMessage('POST', host, msg)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        self.__debug_print(reqMessage)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        
        self.__sendmessage(host, reqMessage)

 送信側だけなので、ブロッキングモードのみです。

 準備ができたら、以下のプログラムをコンソールでコピペ実行するか、pyboard.pyで実行します。
しつこいですが、日本語を使うときは文字コードUTF-8でファイル保存してください。
実行すると、LINEに「へろー」と送信されます。

access_token    = '取得したアクセストークン'

from tiny_line import tiny_line

# 初期化
tl = tiny_line(access_token, debug=True)
# メッセージ送信
tl.notify("へろー")

 

 以下、解説。。。と思ったのですが、何も解説することがありません(笑)。

LINEの場合、時刻情報は不要なので、時刻合わせをする必要はありません。と、無理やり解説。。。

 

micropython on ESP32 でTwitter(その4)

前回の受信プログラムはブロッキング動作のため、受信動作を行うとコールバック以外の処理が行えなくなってしまいます。
そこで、今回はノンブロッキング動作に変更して他の処理を行えるように変更してみます。

とはいえ、tiny_twitterモジュールには既にノンブロッキング動作を行える仕組みが組み込んであります。
以下のプログラムをコンソールでコピペ実行するか、pyboard.pyで実行します。

 

consumer_key    = '取得した Consumer key'
consumer_secret = '取得した Consumer secret'
access_token    = '取得した Access Token'
access_secret   = '取得した Access Token Secret'

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

from tiny_twitter import tiny_twitter
import ujson as json

if platform == 'esp32' :
    # ESP32のときは時刻合わせする
    import ntptime
    import utime
    utime.set_time(ntptime.time())

# データ受信時のコールバック関数
def CallbackFunc(jsn) :
    # print("#### " + json.dumps(jsn))
    print("--------\n" + jsn.get("text", "NO_DATA") + "\n--------")

# 初期化
tw = tiny_twitter(consumer_key, consumer_secret, access_token, access_secret, CallbackFunc, blocking=False, debug=True)


# twitterに接続
param = {"with":"user", "track":"てすとつぃーと"};
tw.userstream(param);


# プロンプト
prmpt = ['-', '\\', '|', '/']
# 表示するプロンプトのインデックス
idx = 0
# 受信ループ
while True :
    if tw.recieve() < 0 :
        # クローズされたら終了
        break
    # プロンプトをくるくる回す
    print(prmpt[idx], end="\r")
    idx = (idx + 1) & 0x03

tiny_twitter.userstream()は、twitterに接続し、レスポンスヘッダを受信した後戻ってきます。

後は定期的にtiny_twitter.recieve()を実行します。
受信データがあれば、ブロッキング動作時と同様にcallbackが呼び出されます。
このプログラムでは、特にほかにやることがないので、プロンプトをくるくると回しています。
フォントが円マーク(¥)でなく、バックスラッシュ(\)なら棒がくるくる回っているように見えると思いますが。。。

 

以下、前回からの変更箇所の説明。

初期化のパラメータに「blocking=False」を追加しています。
パラメータ blockingは、省略時Trueになるように設定されているので、前回のプログラムでは「blocking=True」を指定したのと同等です。

 

tw = tiny_twitter(consumer_key, consumer_secret, access_token, access_secret, CallbackFunc, blocking=False, debug=True)

 

メインループです。
定期的にtw.recieve()をコールして受信処理を行っています。
接続がクローズされると、tw.recieve()は-1を返します。それ以外(受信データがあってもなくても)は0を返します。
接続がクローズされたらループから抜けてプログラム終了です。
ループ内では'-', '\'(半角), '|', '/' を順に表示しています。
実際はここで本来やりたい処理を行います。

# プロンプト
prmpt = ['-', '\\', '|', '/']
# 表示するプロンプトのインデックス
idx = 0
# 受信ループ
while True :
    if tw.recieve() < 0 :
        # クローズされたら終了
        break
    # プロンプトをくるくる回す
    print(prmpt[idx], end="\r")
    idx = (idx + 1) & 0x03

 

明示的にクローズしたいときは、以下のように実行します。

tw.close()

 

micropython on ESP32 でTwitter シリーズはこれにて一旦終了です。

micropython on ESP32 でTwitter(その3)

今回はtweet内容を受信するプログラムを試してみます。
Espruino on ESP32 で Twitter(その4) のmicropython版です。

あまりたくさんのtweetを一度に受信してしまうと、メモリ不足で落ちてしまいます。
それを防ぐため、1つのtweetのデータが大きすぎる場合は、そのデータを破棄します。現在は16kByte以上のデータは破棄するように設定してあります(もっと小さい方が良いかも)。
(引用ツイートのリツイートで色々ユーザ情報が付加されているとデータが大きくなることがあります。サーバ側でフィルタをかけられれば良いのですが。。。)
また、不完全なデータ(一部が抜け落ちている等)が送られてきた場合は、その後のtweetを含めて破棄されることがあります。
(データの終わりが見つからないので、↑の大データリミットに引っかかって破棄されます。このとき、後続のデータも含めて破棄されます。データの先頭や最後をうまく見つける方法があれば良いのですが。。。)

以下のプログラムをコンソールでコピペ実行するか、pyboard.pyで実行します。
twitterに接続したあと、データ受信待ちになります。
以下の設定の場合、自分がtweetするか、「てすとつぃーと」を含むtweetがあった場合、データを表示します。

consumer_key    = '取得した Consumer key'
consumer_secret = '取得した Consumer secret'
access_token    = '取得した Access Token'
access_secret   = '取得した Access Token Secret'

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

from tiny_twitter import tiny_twitter
import ujson as json

if platform == 'esp32' :
    # ESP32のときは時刻合わせする
    import ntptime
    import utime
    utime.set_time(ntptime.time())

# データ受信時のコールバック関数
def CallbackFunc(jsn) :
    # print("#### " + json.dumps(jsn))
    print("--------\n" + jsn.get("text", "NO_DATA") + "\n--------")

# 初期化
tw = tiny_twitter(consumer_key, consumer_secret, access_token, access_secret, CallbackFunc, debug=True)


# twitterに接続
param = {"with":"user", "track":"てすとつぃーと"};
tw.userstream(param);

 

ほとんど前回と同じなので、違う部分だけ説明。

コールバック関数は、基本的に前回と同じですが、今回はtweet内容だけ表示するようにしてあります。

日本語を正常に表示するには、コンソールの日本語モードをUTF-8に設定しておく必要があります。
が、それ以外の言語(韓国語やアラビア語など)が送られてくるとコンソールによっては正常に表示されません。

def CallbackFunc(jsn) :
    # print("#### " + json.dumps(jsn))
    print("--------\n" + jsn.get("text", "NO_DATA") + "\n--------")

たとえば、こんな感じでlangプロパティが"ja"(日本語)か"en"(英語)のときだけ表示するという処理にすると
日本語と英語のtweetのみを表示できるようになります。

def CallbackFunc(jsn) :
    lang = jsn.get("lang", "UNKNOWN")
    text = jsn.get("text", "NO_DATA")
    if lang == "ja" or lang == "en" :
        print("-------- lang: " + lang)
        print(text)
        print("--------")

しかし、langプロパティはサーバ側でtweet内容から判断して付加している情報ですので、完璧ではありません。
たまに韓国語や中国語が混じっている場合があります。
また、日本語や英語であっても絵文字は表示できません。

 

twitterに接続します。
paramに設定できるパラメータは GET user — Twitter DevelopersStreaming API request parameters — Twitter Developers を参照してください。

"language":"en,ja"を追加すれば、↑のようにcallbackで振り分けなくても英語と日本語だけ取得になるような気がします(試してないけど)。

正常に接続されれば、接続されたまま、データを受信するたびにコールバック関数が呼び出されます。
ソケット通信がブロッキング動作のため、tiny_twitter.userstream()はエラーが発生して接続がcloseされるまで戻ってきません。

param = {"with":"user", "track":"てすとつぃーと"};
tw.userstream(param);

 

次回に続く。。。

micropython on ESP32 でTwitter(その2)

さて、早速tweetしてみましょう。

ESP32はmicropython on ESP32 でWi-Fiルータ(or AP)に接続するにしたがってWi-Fiに接続されていて、micropython on ESP32 でTwitter(その1)の手順で各モジュールが改造/インストール済みであるものとします。

 

以下のプログラムをコンソールでコピペ実行するか、pyboard.pyで実行します。
日本語を使用する場合、エディタの文字コードUTF-8を選択しておいてください(これ重要!!)。

consumer_key    = '取得した Consumer key'
consumer_secret = '取得した Consumer secret'
access_token    = '取得した Access Token'
access_secret   = '取得した Access Token Secret'

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

from tiny_twitter import tiny_twitter
import ujson as json

if platform == 'esp32' :
    # ESP32のときは時刻合わせする
    import ntptime
    import utime
    utime.set_time(ntptime.time())

# データ受信時のコールバック関数
def CallbackFunc(jsn) :
    print("#### " + json.dumps(jsn))
    # print("--------\n" + jsn.get("text", "NO_DATA") + "\n--------")

# 初期化
tw = tiny_twitter(consumer_key, consumer_secret, access_token, access_secret, CallbackFunc, debug=True)

# tweetする
query_params = {"trim_user":"true", "include_entities":"false"}
msg = "てすとつぃーと"
tw.tweet(msg, query_params)

成功すれば、「てすとつぃーと《改行》(2017/08/21[Mon] 09:11:18)」のようにtweetされるはずです。

 

 

以下解説。

実行環境のチェックのための値取得です。
ESP32のときは platform に 'esp32'が、Linuxのときは 'linux' が格納されます。

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

 

必要なモジュールをインポートします。
tiny_twittertwitterアクセスのためのモジュールです。これがなければ始まりません。
ujson はJSONデータを処理するためのモジュールです。
twitterのデータはJSONデータで送られてきますが、tiny_twitter内でDICT型に変換されてくるので、DICT型のまま使うなら不要です。
今回はujson.dumps()で文字列に戻すためにインポートしています。

from tiny_twitter import tiny_twitter
import ujson as json

 

ESP32のときは時刻合わせを行います。
micropython on ESP32 で時刻設定を可能にするにあるutimeモジュールの改造と、micropython on ESP32 でNTPサーバから時刻取得にあるntptimeモジュールのインストールが終わっている必要があります。

if platform == 'esp32' :
    # ESP32のときは時刻合わせする
    import ntptime
    import utime
    utime.set_time(ntptime.time())

 

Twitterからのデータを受信したときのコールバック関数です。
パラメータjsnにはDICT型に格納されたJSONデータが入っています。
今回は受け取ったJSONデータをujson.dumps()で文字列に変換して表示しています。
tweet内容だけ表示したい場合はコメントアウトされている側を有効にしてください。

# データ受信時のコールバック関数
def CallbackFunc(jsn) :
    print("#### " + json.dumps(jsn))
    # print("--------\n" + jsn.get("text", "NO_DATA") + "\n--------")

 

tiny_twitterクラスの初期化です。
最初の4つのパラメータがTwitterアクセスのための4つのキーで、5つ目がデータ受信時のコールバック関数(↑で定義)です。
debugパラメータをTrueにすると、モジュール内部のdebug_printでの表示が有効になります。表示したくない場合は省略するかFalseを指定してください。

tw = tiny_twitter(consumer_key, consumer_secret, access_token, access_secret, CallbackFunc, debug=True)

 

query_paramsは APIリファレンス を参照してください。指定しても動くかどうかわかりませんが。。。

msgにはtweetする文字列を設定します。
Espruino版とは異なり、ソケット通信がブロッキング動作のため、tiny_twitter.tweet()はレスポンスを受信し、接続がcloseされるまで戻ってきません。

query_params = {"trim_user":"true", "include_entities":"false"}
msg = "てすとつぃーと"
tw.tweet(msg, query_params)

 

次回に続く。。。

micropython on ESP32 でTwitter(その1)

Espruino on ESP32 で Twitter(その1) - いっぺーちゃんの いろいろやってみよ~のmicropython版です

 

これの元ネタがmicropython版なので、戻ってきた感じですが、Espruino版で入れた種々の変更も取り込んで一から作り直しました。

 

まずは、micropythonのBug fixです(笑)。
以下のパッチをあててmicropythonを再buildしてください。
これは、ハッシュ値の計算を2つ以上同時に行うと2つ目以降の計算値が不正になる、というBugを修正するものです。
mbedtlsでのハッシュ値の計算は、1つだけだとハードウェアを使用しますが、2つ目以降はソフトウェアで行います。これはハッシュ値計算用ハードウェアが1つしかないためです。
このとき、mbedtls_sha*_starts()で計算データの初期値を設定していないと、計算データが不正になります。
ハードウェアを使用する場合は、初期値の設定はハードウェアで自動的に行われるため、ハードウェアを使用する1つ目の計算は正常に行われます。

 

diff --git a/esp32/moduhashlib.c b/esp32/moduhashlib.c
index 6f67aa7..5a6d20b 100644
--- a/esp32/moduhashlib.c
+++ b/esp32/moduhashlib.c
@@ -46,6 +46,7 @@ STATIC mp_obj_t sha256_make_new(const mp_obj_type_t *type,
     mp_obj_hash_t *o = m_new_obj_var(mp_obj_hash_t, char, sizeof(union sha_ctxs));
     o->base.type = type;
     mbedtls_sha256_init(&o->state.sha256);
+    mbedtls_sha256_starts(&o->state.sha256, 0);                // [#/] add
     if (n_args == 1) {
         sha256_update(MP_OBJ_FROM_PTR(o), args[0]);
     }
@@ -58,6 +59,7 @@ STATIC mp_obj_t sha1_make_new(const mp_obj_type_t *type,
     mp_obj_hash_t *o = m_new_obj_var(mp_obj_hash_t, char, sizeof(union sha_ctxs));
     o->base.type = type;
     mbedtls_sha1_init(&o->state.sha1);
+    mbedtls_sha1_starts(&o->state.sha1);               // [#/] add
     if (n_args == 1) {
         sha1_update(MP_OBJ_FROM_PTR(o), args[0]);
     }

 

次はhmacモジュールのインストールです。
参照元にある通り、ここから拝借します。
hmac/hmac.pyの右上の「View」ボタンをクリックしてファイル全体を表示します(その他のファイルは使いません)。
右上の「Raw」ボタンを右クリックして「名前を付けて保存」を選択し、ローカルディスクに保存します。
Ubuntuから直接なら、以下のように実行しても取得できます。

wget https://raw.githubusercontent.com/bynds/micropython-lib/ff74b8cb508199a33e1978d68bd7076f2a536ab0/hmac/hmac.py

 

ダウンロードしたら、以下のパッチを当てます(ESP32ではuhashlibでなくhashlibなので)。
これをupipmなどでインストールします。

--- hmac.py.org	2017-08-19 07:06:27.771787400 +0900
+++ hmac.py	2017-08-19 07:17:43.191131335 +0900
@@ -8,7 +8,11 @@
 #import hashlib as _hashlib
 #PendingDeprecationWarning = None
 #RuntimeWarning = None
-import uhashlib as _hashlib
+#import uhashlib as _hashlib
+try:
+    import uhashlib as _hashlib
+except:
+    import hashlib as _hashlib
 
 trans_5C = bytes((x ^ 0x5C) for x in range(256))
 trans_36 = bytes((x ^ 0x36) for x in range(256))

 

さらにに以下のファイルをダウンロード&解凍し、upipmなどでインストールします。

tiny_twitter_py_20170819.zip

また、Espruino on ESP32 で Twitter(その2) - いっぺーちゃんの いろいろやってみよ~にある通り、4つのキー(Consumer key、Consumer secret、Access Token、Access Token Secret)を用意しておいてください。

 

次回に続く。。。

micropython on ESP32 でNTPサーバから時刻取得

前回、時刻を設定できるようにしたので、今回はNTPサーバから時刻を取得してみます。
これらを組み合わせると、時刻合わせが自動で行えるようになります。

以下のプログラムをntptime.pyという名前で保存し、upipmでインストールします。

import usocket as socket
try:
    import ustruct as struct
except:
    import struct

def time():
    # (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 60 * 60
    NTP_DELTA = 2208988800
    # ntp server
    ntp_host = "ntp.nict.jp"
    
    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1b
    addr = socket.getaddrinfo(ntp_host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # s.settimeout(1)
    res = s.sendto(NTP_QUERY, addr)
    msg = s.recv(48)
    s.close()
    val = struct.unpack("!I", msg[40:44])[0]
    return val - NTP_DELTA

 

以下のように使用します。

import ntptime
import utime

# timeメソッドを呼び出すと経過秒数が取得できます
ntptime.time()
    ==> 1503019707
# その値をそのままutime.set_time()に渡せば時刻を設定できます。
utime.set_time(1503019707)
utime.localtime()
    ==> (2017, 8, 18, 10, 28, 27, 4, 230)

# 通常はこんな感じでまとめれば良いでしょう
utime.set_time(ntptime.time())
utime.localtime()
    ==>(2017, 8, 18, 10, 29, 4, 4, 230)

 

 

以下解説のようなものです。

 

必要なモジュールを読み込みます。

import usocket as socket
try:
    import ustruct as struct
except:
    import struct

 

timeメソッドを定義しています。

def time():
    ....

 

使用する定数です。
NTPサーバからは1900年1月1日00:00:00(UTC)からの経過秒数が送られてきますが、必要なのは1970年1月1日00:00:00(UTC)からの経過秒数なので、それを補正するための定数です。

    # (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 60 * 60
    NTP_DELTA = 2208988800

 

接続するNTPサーバです。別のところに接続したければ変更してください。

    # ntp server
    ntp_host = "ntp.nict.jp"

 

NTPサーバに送信するqueryパケットを生成しています。

    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1b

 

NTPサーバと接続するためのアドレスとソケットを作成します。

    addr = socket.getaddrinfo(ntp_host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

 

QUERYパケットを送信します。

    res = s.sendto(NTP_QUERY, addr)

 

サーバからのレスポンスを受信します。

    msg = s.recv(48)

 

ソケットをクローズします

    s.close()

 

受信データの40~44バイト目に経過秒数データ(正確に表現すると「送信タイムスタンプの整数部」)が入っているので、これを整数に変換しています(ネットワークバイトオーダの整数なので、unpackのフォーマットには「!I」が指定されています)。
変換結果を1970年1月1日00:00:00(UTC)からの経過秒数に変換した値を返します。

    val = struct.unpack("!I", msg[40:44])[0]
    return val - NTP_DELTA

 

 

 

 

 

 

 

micropython on ESP32 で時刻設定を可能にする

ESP32のmicropythonのutimeモジュールにはtimeという時刻(ある時点からの経過秒)を取得するメソッドがあるのですが、時刻を設定する方法が見当たりません。
「無いのなら作ってしまえ、ホトトギス」ということで作ってみました。
ついでに、経過秒から年月日時分秒を取得するlocaltimeメソッド、年月日時分秒から経過秒を取得するmktimeメソッドも作りました(というか、ESP8266からパクってきました)。

以下のパッチをあててmicropythonを再buildしてください。

diff --git a/esp32/modutime.c b/esp32/modutime.c
index f037faa..f1fd43f 100644
--- a/esp32/modutime.c
+++ b/esp32/modutime.c
@@ -31,7 +31,12 @@
 #include <sys/time.h>
 
 #include "extmod/utime_mphal.h"
+#include "lib/timeutils/timeutils.h"
+#include "py/nlr.h"
 
+/// \function time()
+/// get current time.
+/// It returns an integer which is the number of seconds since Jan 1, 1970.
 STATIC mp_obj_t time_time(void) {
     struct timeval tv;
     gettimeofday(&tv, NULL);
@@ -39,10 +44,98 @@ STATIC mp_obj_t time_time(void) {
 }
 MP_DEFINE_CONST_FUN_OBJ_0(time_time_obj, time_time);
 
+/// \function set_time(secs)
+/// set current time.
+/// argument is integer which is the number of seconds since Jan 1, 1970.
+STATIC mp_obj_t time_set_time(mp_obj_t arg) {
+    struct timeval tv;
+    mp_int_t sec = mp_obj_get_int(arg);
+
+    tv.tv_sec = sec;
+    tv.tv_usec = 0;
+    settimeofday(&tv, NULL);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_1(time_set_time_obj, time_set_time);
+
+/// \function localtime([secs])
+/// Convert a time expressed in seconds since Jan 1, 1970 into an 8-tuple which
+/// contains: (year, month, mday, hour, minute, second, weekday, yearday)
+/// If secs is not provided or None, then the current time from the RTC is used.
+/// year includes the century (for example 2014)
+/// month   is 1-12
+/// mday    is 1-31
+/// hour    is 0-23
+/// minute  is 0-59
+/// second  is 0-59
+/// weekday is 0-6 for Mon-Sun.
+/// yearday is 1-366
+STATIC mp_obj_t time_localtime(mp_uint_t n_args, const mp_obj_t *args) {
+    timeutils_struct_time_t tm;
+    mp_int_t seconds;
+    if (n_args == 0 || args[0] == mp_const_none) {
+        struct timeval tv;
+        gettimeofday(&tv, NULL);
+        seconds = tv.tv_sec;
+    } else {
+        seconds = mp_obj_get_int(args[0]);
+    }
+    seconds -= 946684800;        // 946684800 means seconds of 1st January 1970 00:00:00 to 1st January 2000 00:00:00
+    // reflect TimeZone
+    seconds += 9 * 60 * 60;                 // FIX to "JST"
+
+    timeutils_seconds_since_2000_to_struct_time(seconds, &tm);
+    mp_obj_t tuple[8] = {
+        tuple[0] = mp_obj_new_int(tm.tm_year),
+        tuple[1] = mp_obj_new_int(tm.tm_mon),
+        tuple[2] = mp_obj_new_int(tm.tm_mday),
+        tuple[3] = mp_obj_new_int(tm.tm_hour),
+        tuple[4] = mp_obj_new_int(tm.tm_min),
+        tuple[5] = mp_obj_new_int(tm.tm_sec),
+        tuple[6] = mp_obj_new_int(tm.tm_wday),
+        tuple[7] = mp_obj_new_int(tm.tm_yday),
+    };
+    return mp_obj_new_tuple(8, tuple);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(time_localtime_obj, 0, 1, time_localtime);
+
+/// \function mktime()
+/// This is inverse function of localtime. It's argument is a full 8-tuple
+/// which expresses a time as per localtime. It returns an integer which is
+/// the number of seconds since Jan 1, 2000.
+STATIC mp_obj_t time_mktime(mp_obj_t tuple) {
+
+    size_t len;
+    mp_obj_t *elem;
+
+    mp_obj_get_array(tuple, &len, &elem);
+
+    // localtime generates a tuple of len 8. CPython uses 9, so we accept both.
+    if (len < 8 || len > 9) {
+        nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_TypeError, "mktime needs a tuple of length 8 or 9 (%d given)", len));
+    }
+
+    mp_int_t t = timeutils_mktime(  mp_obj_get_int(elem[0]),
+                                    mp_obj_get_int(elem[1]),
+                                    mp_obj_get_int(elem[2]),
+                                    mp_obj_get_int(elem[3]),
+                                    mp_obj_get_int(elem[4]), 
+                                    mp_obj_get_int(elem[5])    );
+    // reflect TimeZone
+    t -= 9 * 60 * 60;               // FIX to "JST"
+    t += 946684800;                 // 946684800 means seconds of 1st January 1970 00:00:00 to 1st January 2000 00:00:00
+
+    return mp_obj_new_int_from_uint(t);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(time_mktime_obj, time_mktime);
+
 STATIC const mp_rom_map_elem_t time_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_utime) },
 
     { MP_ROM_QSTR(MP_QSTR_time), MP_ROM_PTR(&time_time_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_time), MP_ROM_PTR(&time_set_time_obj) },
+    { MP_ROM_QSTR(MP_QSTR_localtime), MP_ROM_PTR(&time_localtime_obj) },
+    { MP_ROM_QSTR(MP_QSTR_mktime), MP_ROM_PTR(&time_mktime_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep), MP_ROM_PTR(&mp_utime_sleep_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep_ms), MP_ROM_PTR(&mp_utime_sleep_ms_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep_us), MP_ROM_PTR(&mp_utime_sleep_us_obj) },

 

ESP8266などではtime()で取得できる経過秒は2000年1月1日 00:00:00(UTC) からの経過秒となっていますが、ここでは他のシステム同様1970年1月1日00:00:00(UTC)からの経過秒にしてあります。
2000年1月1日 00:00:00(UTC) を基準にしたい場合は、946684800を加減している部分を削除すると変更できると思います(たぶん)。

また、localtime/mktimeのタイムゾーンは+9(JST)に固定してあります。
これも 9*60*60 を 加減している部分を変更すれば任意のタイムゾーンに変更できます(たぶん)。

 

以下のように使用します。

import utime

# 年月日時分秒とダミー2個のタプルをmktimeに入力するとその時刻の経過秒が取得できます。
# 2017年8月18日8時3分0秒の場合
# メソッド呼び出しのための括弧とタプル作成のための括弧で括弧が2重になっています。1重にするとエラーになりますので、注意してください
utime.mktime((2017, 8, 18, 8, 3, 0, 0, 0))
    ==> 1503010980

# 時刻の設定 mktimeで取得した値をset_timeで設定します。
utime.set_time(1503010980)

# この2つを組み合わせて、以下のようにすることも可能です。
utime.set_time(utime.mktime((2017, 8, 18, 8, 3, 0, 0, 0)))


# timeで現在時刻の経過秒が取得できます
utime.time()
    ==> 1503010982

# localtimeをパラメータなしで実行すると現在時刻を年月日時分秒形式のタプルで取得できます。
# 後ろから2個目は曜日で、0:月曜、1:火曜、・・・6:日曜 を表します。
# 最後はその年の1月1日からの経過日数です。
utime.localtime()
    ==> (2017, 8, 18, 8, 3, 2, 4, 230)



# localtimeをパラメータを指定して実行するとパラメータで指定した経過秒を年月日時分秒形式に変換できます。
utime.localtime(1503010980)
    ==> (2017, 8, 18, 8, 3, 0, 4, 230)

# パラメータなしのutime.localtime()は、utime.localtime(utime.time())と等価です。

 ここで設定した時刻はソフトリセットでは保持されますが、ハードリセットや電源OFFでは保持されませんので、再度時刻を設定してください。

ちなみに、 mktimeは1970年1月1日09:00:00 以前の値を与えると間違った値を返します。