見出し画像

【タ】ORANGE pico でタイマー

#クリエイターフェス 6日目、テーマは「タ」である。

タイマー

今回は、ORANGE pico でキッチンタイマーを実装してみた。

フォントの用意

ORANGE pico の広い画面の中に普通の描画コマンドを用いて普通の文字を描画しても、小さくて見栄えが悪い。
そこで、文字を大きく表示できるよう、フォントを用意した。

ORANGE pico では、gput コマンドによりメモリー配列から正方形のパターンを描画することができる。
実験を行ったところ、このコマンドでは、以下の順序で描画が行われることがわかった。

  1. 上の行から下の行の順番

  2. 行の中では、左から右の順番

  3. 1バイトの中では、上位のビットほど左

描画する内容は、1ビットで1ピクセルを表し、1の場合は描画を行い、0の場合は描画を行わない。

今回は、横48ピクセル、縦96ピクセルの大きさで数字を書き、これを縦に0~9の10個並べた画像を用意した。
この画像を以下のPythonプログラムで処理し、連続する 0 (白) や 1 (黒) の数を(基本的に)1文字で表した。(ランレングス圧縮)

import sys
from PIL import Image

if len(sys.argv) != 2:
	sys.stderr.write("Usage: bw-rle.py input-file")
	sys.exit(1)

img = Image.open(sys.argv[1]).convert("L")
(width, height) = img.size

currentIsWhite = True
count = 0
data = []
# debug = []
for y in range(height):
	for x in range(width):
		isWhite = img.getpixel((x, y)) >= 128
		# debug.append(255 if isWhite else 0)
		if isWhite == currentIsWhite:
			count += 1
		else:
			data.append(count)
			count = 1
			currentIsWhite = isWhite

data.append(count)

print(data)

# Image.frombytes("L", (width, height), bytes(debug)).save("debug.png")

result = ""
for value in data:
	full = value // 0x5e
	left = value % 0x5e
	puttern = [0x5e for _ in range(full)]
	if len(puttern) == 0 or left > 0:
		puttern.append(left)
	if len(puttern) >= 2 and (puttern[-1] == 0x02 or puttern[-1] == 0x3c):
		puttern[-2] -= 1
		puttern[-1] += 1
	if len(puttern) == 1:
		if puttern[0] == 2:
			result += '\\"'
		elif puttern[0] == 0x3c:
			result += "! ["
		else:
			result += chr(puttern[0] + 0x20)
	else:
		result += chr(puttern[0] + 0x20)
		for c in puttern[1:]:
			result += " " + chr(c + 0x20)

print('"' + result + '"')

動かない場合は、以下のコマンドで使用している画像処理ライブラリをインストールすると動く可能性がある。

pip install Pillow

さらに、32ピクセル×32ピクセルの「m」と「s」の画像も用意し、同様に処理した。
これらのデータを文字列として ORANGE pico 用のプログラムに埋め込み、デコードするプログラムも用意することで、画像をメモリー配列に展開し、描画用のフォントとして用いた。

仕様の設定

キッチンタイマーを実装するにあたり、手元のキッチンタイマーの動作を調査した。
その結果を参考に、今回のキッチンタイマーは以下の仕様とした。

  • 停止中・カウントダウン状態 (初期状態)

    • 分ボタン/秒ボタンを1個押すと、

      • 対応する分/秒を1増やし、更新後の分/秒を記録する

    • 分ボタンと秒ボタンを両方押すと、

      • 分および秒をともに0にする

    • スタートボタンを押すと、

      • 分と秒がともに0の場合は、カウント中・カウントアップ状態に移行する

      • そうでない場合は、カウント中・カウントダウン状態に移行する

  • カウント中・カウントダウン状態

    • 1秒ごとに、

      • 残り時間を1秒減らす

      • 更新後の残り時間が0の場合は、鳴動中状態に移行する

    • 分ボタンと秒ボタンを両方押すと、

      • 分および秒をともに0にする

      • 停止中・カウントダウン状態に移行する

    • スタートボタンを押すと、

      • 停止中・カウントダウン状態に移行する

  • 鳴動中状態

    • 適当な間隔でビープ音を鳴らす

    • 分ボタンと秒ボタンを両方押すと、

      • 停止中・カウントダウン状態に移行する (分および秒は0である)

    • スタートボタンを押すと、

      • 分および秒を記録しておいた値にする

      • 停止中・カウントダウン状態に移行する

  • 停止中・カウントアップ状態

    • 分ボタン/秒ボタンを1個押すと、

      • 対応する分/秒を1増やし、更新後の分/秒を記録する

      • 停止中・カウントダウン状態に移行する

    • 分ボタンと秒ボタンを両方押すと、

      • 分および秒をともに0にする

      • 停止中・カウントダウン状態に移行する

    • スタートボタンを押すと、

      • カウント中・カウントアップ状態に移行する

  • カウント中・カウントアップ状態

    • 1秒ごとに、残り時間を1秒増やす

    • 分ボタンと秒ボタンを両方押すと、

      • 分および秒をともに0にする

      • 停止中・カウントダウン状態に移行する

    • スタートボタンを押すと、

      • 停止中・カウントアップ状態に移行する

実装

10 mptr 0:bit=0:byte=0:bitcount=0:loaded=0:cls:white=rgb(255,255,255):black=rgb(0,0,0):gprint 88,96,"Loading",white
20 s$="~ ~ ~ l#J)D.@1>3<5;697988+#*7)'*6)()6(*)4)*)4)+)3(,)2)-(2)-(1).(1).)0)/(/)0).)0).)1),)2),)2),)2)+)3)+)4(+)4)))5)))5)))5)))5)))5)))5))(7(()7)')7)')7)')7)')7)')7)')7)')7)')7)')7)()6)()6)()6)()6)()6)))5)))5)))5(*)4)*)4)+)3)+)3)+)3(,)2)-)1)-)1)-*"
30 l=len(s$)
40 for i=1 to l
50 c=asc(mid$(s$,i,1))-&H20
60 if c==0 then goto 120
70 for j=1 to c
80 byte=(byte<<1)|bit
90 bitcount=bitcount+1
100 if bitcount==8 then b=byte:byte=0:bitcount=0:mdata b
110 next
120 bit=1-bit
130 next
140 gprint 88+8*(7+loaded),96,".",white:loaded=loaded+1:if loaded>1 then return
150 s$="/)/)/)/*-*0*,)1+**2+'+4<5:6:7896;4=1A-E)~ ~ ~ ~ ~ ~ ~ ~ ~ ~ -%J'I'H)G)F*F*E+E+D,D,C-C-C-B.B.A/A/A/A/A/A/B.D!\")G)G)G)G)G)G)G)G)G)H(H)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)H(H'I'J%M!~ ~ ~ ~ ~":gosub 30
160 s$=" ~ ~ ~ +,A2;78:4=2>2?0@/A.-,*-+/)-)1),*1),)2)+)3)+)3)+)3)*)4)+(4)+'5)+'5),%6).!8)G)G)G)G(G)G)G)F)G)G)G(G)G)G(G)G)G)F)G)F*F)F*F)F)G)F)G)F*E*F*E*E*E+D+D+E*E*E+D+D+D+D+D+C-B-A/@C,G)H(H(I'H)G+D:4~ ~ ~ ~ ~ ~ ~ ~ Q#I*C.@1>3;698796;40\"*4-":gosub 30
170 s$="&*2,))1,+)0*-)/*/(.+/)-*0)-)1),)2),)3(-(3),'4),'4)-%5)/!7)G)G)G)F)F*F)F*E*F*E*E*E+D+D+D,C,A.?0:5878797997:6;6;7:B/D,F+F*G*G)G)G)H)G)G)G)G)H)/%3).'2).'2).(1)-)1).)0).*/).+.)/+++0?2>2=4;6:7896<1A,~ ~ ~ ~ ~ ~ ~ !%J'I'H)G)G(G)G(H(G)G)G)":gosub 30
180 s$="G(G)G)G)G(G)G)G(G)-%5),'4(-'3)-(2),)2(-)1)-(2),)2),)2),)2),(3(,)2),)2),)2),)2(-(2),)2),)2),)2),)1)-(2),)2),)2),)2),)2),(3(,)2),)2),)2>2G)H'J'I'J&I(H*E;3=)G(G)G)G)G(G)G)G(G)G)F*F)G)F)G)G)G(G)H(H'I'J%M!~ ~ ~ ~ ~ ~ ~ Q?0C-D+E+F*E+E+D,B":gosub 30
190 s$=".)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)G)F)((7)$/4)!33?1@/A/B.C-D,2(*,.-*+,0)*+3))*4)*(6))'7))'8))%9)+!;)G)G)G)G)G)G)G)G)G)F)G)G)G)G)F)G)G)F)G)0!5*-'2)-*.+-,++..(,.A/@1>4;78:5=2@.E(~ ~ ~ ~ ~ ~ ~ ~ 3&G*E+D-B-B.B-B-B,D*E*F)F*F)F*E*F)G)":gosub 30
200 s$="F)G)F)G)G(G)G)F)G)G(G)G)G(G)G)G)G)!&@3<7997:5<4>2?1?1,&.0),,/)-+/).+.)/*.)0).)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1)-)1(/)/)/)/)/)/)0).)0).(2),)2*+)2+(+3<4<5:7987:5<3?/C)~ ~ ~ ~ ~ ~ G!LD+H(I'I&K%J&J&I'I')6)()6)()":gosub 30
210 s$="5)()6)()6)()5)))5)))5)*(5(+'5)+'5),%6).!8(G)G)G(G)G)G(G)G)G(G)G)G)G)G(G)G)G)G)G(G)G)G(G)G)G(G)G)G(G)G)F)G)G(G)G)G(G)G)F)G)G(G)G)F*F)G)F)G)G(G)G)F)G)G)F)G)G(H(H'J%L#~ ~ ~ ~ ~ ~ ~ a)D/?3<5996;4=3=2?0,)+0*,*0).*.)0).)0*-(2),)3)+)3)+)3)":gosub 30
220 s$="+)3)+)3)+)3)+)3)+)3)+)3)+)3)+)2),)2),)0*-)/+-).,.(-,/)*./)).1((.2)%.4)$.6)!/7796;4<2?0A-B.A0?2=5:7897+\"-5+%,3+',2*),0*++0)-+/).+-)0*-)1*,)2),)2*+)3)+)4(+)4)*)4)+(4)+)3)+)3),(3),)1*,)0+-),.-*&2.B/@0?2=4:77:4=/C(~ ~ ~ ~ ~ ~ ~ ~ ,)D0?":gosub 30
230 s$="4:87;5=2?0A.B.+(0,*.,,)1*,)2)+)3)+)2*+)1+*)2*+)1++)1*,)0+,)/,,)/+-).,-).+.)-,.)-,.),,/)+-/))//)'10?1?1>3=3=4;6:8-!*:)#)G)G(G)G)G(G)G)G)G(G)G)G(G)G)G)G(G)G)G)G)G(G)G)G)F)G)G)F*F)G)F)G)G)F)G)G)G(H(I&L\"~ ~ ~ ~ ~ F k!6#!%#%'2&2&&\"&!$%":gosub 30
240 s$="$$%##%#&#$#%#&#$$$#&#%#$#&#%#$#&#%#$#&#%#$#&#%#$#&#%#$#&#%#$#&#%#%\"'\"&\"~ D'/,+.)&$&'%)$&$+#&#-\"&#5#5%4(1--,0)5#5$&!.#%$,#%&(%&1(0++>":gosub 30
250 cls:repstart=700:repinterval=100:beepnote=36:beeplen=50
260 dim xpos(4)=[150-24-48*2,150-24-48,170,170+48]
270 dim drawn(4)=[-1,-1,-1,-1]:dim todraw(4)
280 min=0:sec=0:mstart=0:sstart=0:mstatus=0:sstatus=0:startstatus=0:resetted=0:running=0:runstart=0:ringing=0:ringstart=0:minset=0:secset=0:countup=0:cltms
290 gput 150-24,100+24,24,6*48*2*10,white
300 gput 170+48*2,100+24,24,6*48*2*10+3*24,white
310 todraw(0)=min/10:todraw(1)=min%10:todraw(2)=sec/10:todraw(3)=sec%10
320 for i=0 to 3
330 if drawn(i)==todraw(i) then goto 400
340 for x=0 to 47
350 line xpos(i)+x,100-48,xpos(i)+x,100+48,black
360 next
370 gput xpos(i),100-48,48,6*48*2*todraw(i),white
380 gput xpos(i),100,48,6*48*2*todraw(i)+6*48,white
390 drawn(i)=todraw(i)
400 next
410 t=tickms():k=kbstatus():mpress=k&1:spress=k&8:startpress=k&2
420 if resetted then goto 600
430 if mpress && spress then cltms:resetted=1:min=0:sec=0:minset=0:secset=0:running=0:ringing=0:countup=0:beep beepnote,beeplen:goto 310
440 if running then goto 620
450 if mpress==0 then mstatus=0:goto 510
460 if mstatus==0 then mstatus=1:cltms:mstart=tickms():goto 500
470 if mstatus==1 && t>=mstart+repstart then mstatus=2:mstart=mstart+repstart:goto 500
480 if mstatus==2 && t>=mstart+repinterval then mstart=mstart+repinterval:goto 500
490 goto 410
500 min=(min+1)%100:countup=0:minset=min:secset=sec:beep beepnote,beeplen:goto 310
510 if spress==0 then sstatus=0:goto 570
520 if sstatus==0 then sstatus=1:cltms:sstart=tickms():goto 560
530 if sstatus==1 && t>=sstart+repstart then sstatus=2:sstart=sstart+repstart:goto 560
540 if sstatus==2 && t>=sstart+repinterval then sstart=sstart+repinterval:goto 560
550 goto 410
560 sec=(sec+1)%60:countup=0:minset=min:secset=sec:beep beepnote,beeplen:goto 310
570 if startpress==0 then startstatus=0:goto 410
580 if startstatus==0 then startstatus=1:cltms:runstart=tickms():running=1:countup=countup || (min==0 && sec==0):beep beepnote,beeplen:goto 310
590 goto 410
600 if mpress==0 && spress==0 && startpress==0 then resetted=0:mstatus=0:sstatus=0:startstatus=0:goto 310
610 goto 410
620 if ringing==0 then goto 670
630 if startpress==0 then startstatus=0:goto 650
640 if startstatus==0 then startstatus=1:cltms:beep beepnote,beeplen:running=0:ringing=0:min=minset:sec=secset:goto 310
650 if t>=ringstart+beeplen then beep beepnote,beeplen:ringstart=ringstart+beeplen
660 goto 410
670 if startpress==0 then startstatus=0:goto 690
680 if startstatus==0 then startstatus=1:cltms:beep beepnote,beeplen:running=0:goto 310
690 if t<runstart+1000 then goto 410
700 runstart=runstart+1000
710 if countup goto 760
720 if sec>0 then sec=sec-1:goto 740
730 if min>0 then min=min-1:sec=59:goto 310
740 if sec==0 && min==0 then cltms:ringstart=tickms():beep beepnote,beeplen:ringing=1
750 goto 310
760 if sec<59 then sec=sec+1:goto 310
770 sec=0:min=(min+1)%100:goto 310

実装のポイント

  • 数字を毎回描画するのは無駄が多く、ちらつきの原因にもなるので、描画した数字を記録し、更新された場合のみ描画を行う。

  • tickms() のカウントは beep で音を鳴らしている間止まるようである。そのため、特に鳴動中状態においてこれを考慮した時間の設定を行う。

使い方

キーボードを接続して操作する。UART経由では操作できない。
HetaPad でも操作できるはずである。

  • 左矢印キー:分

  • 下矢印キー:秒

  • 右矢印キー:スタート

実行の様子

実行を開始すると、まずはフォントのロード (デコード) を行う。これは10秒ほどかかる。

フォントをロード中の画面

ロードが完了すると、分と秒が表示される。

フォントのロードが完了した初期画面

キーボードの左矢印 (分) や下矢印 (秒) を押すことで、時間を設定できる。
キーを押しっぱなしにすることで、時間を連続で増やすことができる。

時間を設定した画面

改造のヒント

たとえば、以下の変更を行うことが考えられる。
これらは読者への宿題とする。

  • ゲームコントローラーやI/O端子への入力で操作できるようにする

  • 現在の鳴動中状態では「ピピピピピピピピ…」と連続して音が鳴るが、「ピピピピ、ピピピピ、…」と断続的に鳴るようにする

  • 鳴動中状態かどうかをI/O端子に出力し、外部機器との連携がしやすいようにする

ライセンス

今回のプログラムと解説 (「タイマー」節の内容) は、CC BY 4.0 でライセンスする。


この記事が気に入ったらサポートをしてみませんか?