Raspberry Pi pico Wをスマホから遠隔操作する。

やりたいこと

以下の通り。なお、Wi-Fiアクセスポイントは、iPhoneによるテザリングを想定している。

  • Raspberry Pi pico Wが移動していると仮定する。
  • Raspberry Pi pico Wが、登録しておいたWi-Fiアクセスポイントに近づいたら、自動で接続する。
  • Wi-Fiアクセスポイントに接続できたら、SLACKに通知する。
  • 通知をみら、指定されたURLにアクセスすると、ブラウザにON/OFFボタンが表示される。
  • ボタンをクリックすると、出力端子が作動する。

コード

import machine
import network
import uasyncio
import urequests
from microdot import Microdot, Response

SSID = "YOUR_WI-FI_SSID"
PW = "WI-FI_PASSWORD"

SLACK_URL = "https://hooks.slack.com/services/***********/***********/************************"

async def run_web_server():
    app = Microdot()
    Response.default_content_type = 'text/html'
    
    htmldoc_home = "<html><head><style type="text/css"><!--  table {width: 100%; height: 100%;} td {text-align: center;} button {width: 90%; height: 80%; font-size: 64px;} --></style><title>Wireless controller</title></head><body><table><tr><td><button onclick="location.href='./relay/on'">On</button></td><td><button onclick="location.href='./relay/off'">Off</button></td></tr></table></body></html>"
    htmldoc_on = "<html><head><title>Wireless controller</title></head><body>Turned on.</br><a href="../../">Return</a></body></html>"
    htmldoc_off = "<html><head><title>Wireless controller</title></head><body>Turned off.</br><a href="../../">Return</a></body></html>"
    
    relay1 = machine.Pin(21, machine.Pin.OUT)
    relay2 = machine.Pin(22, machine.Pin.OUT)
    relay1.off()
    relay2.off()
    
    @app.get('/')
    async def _index(request):
        return htmldoc_home
    
    @app.get('/relay/<status>')
    async def _relay(request, status):
        if status == 'on':
            relay1.on()
            relay2.on()
            return htmldoc_on
        elif status == 'off':
            relay1.off()
            relay2.off()
            return htmldoc_off
        
        return 'Invalid status.'
    
    print('Web server is running.')
    app.run(port = 80)
    
async def loop():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    
    led = machine.Pin('LED', machine.Pin.OUT)
    
    while True:
        if wlan.isconnected() == False:
            print("Connecting to the Wi-Fi router.", end="")
            count = 0
            
            while wlan.isconnected() == False:
                if count == 0:
                    wlan.disconnect()
                    wlan.connect(SSID, PW)
                
                led.value(1)
                print(".", end="")
                await uasyncio.sleep(0.25)
                led.value(0)
                await uasyncio.sleep(0.25)
                count += 1
                
                if count > 10:
                    count = 0
                    
            print(" Connected.")
            led.value(1)
            
            wlan_status = wlan.ifconfig()
            print(f'- IP Address: {wlan_status[0]}')
            print(f'- Netmask: {wlan_status[1]}')
            print(f'- Default Gateway: {wlan_status[2]}')
            print(f'- Name Server: {wlan_status[3]}')
            print()
            print('Please access: http://{}/'.format(wlan.ifconfig()[0]))
            print()
            
            print('I will nortify to the Slack.')
            data = {"text" : "Hello! Please access: http://" + wlan_status[0] +"/", "username" : "Raspberry Pi"}
            response = urequests.post(SLACK_URL, json=data)
            print(f'{response.status_code} {response.text}')
            response.close()
        
        await uasyncio.sleep(1)
    
async def main():
    uasyncio.create_task(loop())
    await run_web_server()
    
if __name__ == '__main__':
    uasyncio.run(main())

参考になったサイト

記事を参考にRaspberry Pi Zero Wにumap2をインストール

基本的にはこの記事のとおりである。 qiita.com

ただし、一部を自分で読み替えたため、違う点を以下にメモしておく。

ポイント1(機材)

以下の機器を用意する。

  1. Raspberry Pi Zero WH
  2. microUSB OTGケーブル
  3. miniHDMI(m)-HDMI(f)変換アダプタ
  4. SDカード 32GB
  5. USBハブ
  6. USBマウス
  7. USBキーボード
  8. microUSBケーブル(電源用)
  9. ラップトップPC
  10. USB接続SDカードライター

加えて、ソフトウェアをインターネットからダウンロードするためのWi-Fi環境も必要である。

  • Raspberry Pi Zero 2 Wを購入してはいけない。なぜならば、後にstart_gadgetfs_RaspiZeroW.shを実行するときに、unknown filesystem typeというエラーがでてしまうため。
  • SDカードは、SDHDカードの上限である32GBを選ぶのが無難。64GB以上は規格がSDXDになり、書き込みに失敗するという記事が存在する。
  • ヒートシンクが必要という記事が存在するが、この用途だけなら不要と考える。
  • SDカードライターはPCに備え付けのものがあれば最低限は事足りるが、作成したOSイメージをSDカードへバックアップするときのため、外付けのものを用意するとよい。

ポイント2(SDカードへの書込み その1)

"2022-09-22-raspios-bullseye-armhf-full.img.xz"という古いイメージをダウンロードし、Raspberry Pi imagerの「カスタムイメージを使う/Use custom」というメニューでSDカードへ書き込む。

  • 最新のRecommendedのOSを選択してはいけない。Umap2は、2016年ごろに書かれたプログラムである。最新のRaspberry Pi OSを選択してしまうと、umap2のインストールするときに用いるpython-devやpython-setuptoolsが容易にインストールできない。最新のRaspberry Pi OSには、Pythonの2系がインストールしにくいため。
  • 私の手元の環境だと、SDカードへの書き込みに何度も失敗する。根気よく繰り返す。

ポイント3(SDカードへの書込み その2)

SDカードをRaspberry Piに挿入する前に、SDカードの中のファイルconfig.txtの末尾の[all]の下に、以下を追記する。

dtoverlay=dwc2
  • 記事では、enable_uart=1も追記しているが、USB-UART変換ケーブルを持っていないのであれば、追記不要である。Raspberry Pi Zero WにはWi-Fi経由でのssh接続できるため。ただし、sshで接続するにはWi-Fiルーター(スマホテザリングでも代用可)が必要なため、初心者にも使いやすい手順書にしたい場合は、変換ケーブルを購入するとよい。 www.monotaro.com

  • Raspbery PiのUSBポートは、ホストとして駆動している。一方、umap2によりほかのデバイスのUSBポートをスキャンするときは、Raspbery PiのUSBポートをクライアントにしておかなければならない。dwc2を追記しておくと、クライアントとしても使用できるようになる。

  • 公式ページの以下の記述によると、cmdline.txtにmodules-load=dwc2を追記するようにも指示があるが、こちらは追記しなくても機能する。

umap2/gadget at master · nccgroup/umap2 · GitHub

ポイント4(初回セットアップ)

SDカードをRaspberry Piに挿入してから、USBポート(電源用は外側)にUSBケーブルを挿入する。

初回起動時のセットアップにおいて、海外の従業員がそのまま読めるよう、「Use English Language」にチェックを入れる。

アカウントは、伝統的にユーザー名に「pi」、パスワードに「raspberry」を設定する。

ソフトウェアのインストールや、Wi-Fi経由でのSSH接続ができるよう、Wi-Fiの設定をしておく。(Raspberry Pi ZeroにはUSBポートが実質1個しかないので、USBポートをスキャンするときは、USBキーボードやマウスが接続できなくなる。)

アップデートはスキップする。

ポイント5(パッケージリストの更新)

初回起動時は数分間待つ。

そのあと、パッケージリストを更新する。

sudo apt update
  • 初回起動の直後はpackagekitdというプロセスが実行されていて、apt updateに失敗するため。数分間待ってから実行する。
  • apt upgrade は実行しなくてよい。

ポイント6(Pytyon 2系のインストール)

Pythonの2系をインストールする。

sudo apt -y install python-dev
sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
sudo update-alternatives --config python
  • sudo update-alternatives --config pythonをした後の出力が記事とは違う。2.7系しかインストールしていないため、選択肢が表示されない。1つしか表示されないので、この手順は不要な気がする。要追加検証。

pythonのバージョンが2.7.1になっていることを確認する。

python --version

ポイント7(環境変数の追加)

環境変数を追加する。ユーザー名がpiの場合は以下の通り。参考記事では.bashrcに環境変数を追加しているが、初心者には..bashrcが複雑なため、bash_profileを新規作成して、追加するほうがよい。

cd ~
touch .bash_profile
vi .bash_profile
PATH=/home/pi/.local/bin:$PATH
source ~/.bash_profile

ここからはだいたい記事と同じでよい。フォルダ名が間違っており、正しくはumapではなくumap2である。

ポイント8(コマンドラインの取得)

ここからは試験のたびに実行する。

USB-UART変換ケーブルを持っている人は、それでラップトップPCとRaspberry Piとを接続すればよい。

持っていない人は、実行前にSSHを有効化する。Raspberry Pi ZeroにはUSBポートが実質1個しかない。スキャンしているときはUSBキーボードが使えなくなるためである。 左上のメニュー -> Preference -> Raspberry Pi Configuration -> Interfaces tab -> SSHを有効にする。 ラズパイへのssh接続を有効化する方法(2020年版) #RaspberryPi - Qiita

同じWi-Fiに接続している別のPCにおいて、Teratermを起動し、SSHで接続する。Raspberry piIPアドレスはifconfigで調べる。user/passwordでログインする。

ポイント9(スキャン)

試験する機器をUSBポート(内側に)接続する。

facedancerを使用しないため、umap2を実行する前に、gadgetfsモジュールをロードする必要がある。

cd ~/umap/gadget
pwd
ls -l
sudo sh start_gadgetfs_RaspiZeroW.sh
cd ~/umap/data
sudo umap2scan -P gadgetfs
  • 参考として、Facedancerで実行するときはumap2scan -P fd:/dev/ttyUSB0というコマンドをつかう。

離(以下は個人的なメモである。)

最新のRaspberry Pi OSではsudo apt -y install python-devによるpython2のインストールができない。代わりに以下の手順でpython2をインストールすることができる。ただし、この手順では、python-setuptoolsがインストールできないため、umap2のsetup.pyが実行できない。

こちらを参考に、まずpyenvを使えるようにする。 Raspberry PiにpyenvをインストールしてPythonをバージョン管理 #RaspberryPi - Qiita

必要なライブラリをインストールする。たまにupdateに失敗するので複数回行うとよい。

sudo apt update
sudo apt upgrade
sudo apt install -y git openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev

pyenvをクローン。

git clone https://github.com/yyuu/pyenv.git ~/.pyenv

bashの設定をするため、.bash_profileを作成する。

cd ~
touch ./.bash_profile
vi ./.bash_profile
sudo vi ~/.bash_profile

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

source ~/.bash_profile

次のコマンドでpyenvのバージョンが表示されればインストールが完了している。

pyenv --version

2系の最新版をインストールする。

pyenv install 2.7.16

2系を標準に変更する。

python --version
python3 --version
pyenv global 2.7.16
python --version
python3 --version

ちなみに、ここでsudoをつけてget-pip.pyを実行すると失敗する。

wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
python get-pip.py
pip install requests
pip install bitstring
git clone https://github.com/BinyaminSharet/Mtp.git
cd Mtp
python setup.py install

ここまではできるが、次のpython-setuptoolsのインストールができずに手詰まりになる。

sudo apt install python-setuptools

エンジニアは、何も書かないのが正解

書き出し

お世話になっております。
ご返信ありがとうございます。
突然のメールで失礼いたします。御社のウェブサイトを拝見し、はじめてメールを差し上げます。
~様からの紹介でメールを差し上げます。
日頃から~をご利用いただき、誠にありがとうございます。
平素は格別のお引き立てをいただき、ありがとうございます。
ご無沙汰しております。
その後、いかがおすごしでしょうか。
立て続けのご連絡で失礼いたします。

書き終わり

どうぞ、よろしくお願い申し上げます。
何卒、よろしくお願い申し上げます。
引き続き、よろしくお願い申し上げます。
今後とも、お力添えのほど、よろしくお願い申し上げます。
勝手なお願いで大変恐縮ですが、よろしくお願い申し上げます。
ご確認のほどよろしくお願いいたします。
お手数ですが、ご返事いただければ幸いでございます。
ご検討のほど、よろしくお願い申し上げます。
お近くにお越しの際はぜひお立ち寄りください。
お会いできることを楽しみにしています。
ご参加をお待ちしております。
取り急ぎ、よろしくお願いいたします。
取り急ぎ、お礼申し上げます。
ご不明な際は、お気軽におっしゃってください。
今から楽しみでございます。
お礼を兼ねて失礼いたします。

president.jp

なろう作品をKindleで読めるePub形式に変換

つかいかた

1. この記事の下部にあるRubyスクリプトを、任意のディレクトリに"narou2epub.rb"というファイル名で保存する。

2. 以下のようにコマンドラインで実行すると、epubファイルができる。[ncode]には、なろう作品のものを指定する。

$ ruby narou2epub.rb [ncode]

注記:未インストールのときは、事前に一度だけNokogiriとRubyzipのインストールが必要。

$ gem install nokogiri
$ gem install rubyzip

3. できたファイルをメールに添付し、Kindle端末のメールアドレスに送信する。

Rubyスクリプト

require 'open-uri'
require 'nokogiri'
require 'zip'

def scrape(url)
  opt = {}
  opt['Cookie'] = "over18=no"
  charset = nil
  
  html = open(url, opt) do |io|
    charset = io.charset
    io.read
  end
  
  return Nokogiri::HTML.parse(html, nil, charset)
end

def replace(text)
  text.gsub!(/<p id="L?\d+">/, '<p>')
  text.gsub!('<br>', '<br />')
  text.gsub!(/<img .*?>/, '(画像削除)')
  text.gsub!(/<a .*?>/, '')
  text.gsub!("</a>", '')
  return text
end

def puts_novel(io, doc)
  results = doc.xpath('//div[@id="novel_p"]')
  unless results.length == 0
    io.puts replace(results.inner_html)
    io.puts '<hr />' 
  end
  
  results = doc.xpath('//div[@id="novel_honbun"]')
  io.puts replace(results.inner_html)
  
  results = doc.xpath('//div[@id="novel_a"]')
  unless results.length == 0
    io.puts '<hr />' 
    io.puts replace(results.inner_html)
  end
end

ncode = (ARGV[0].nil?) ? 'n6316bn' : ARGV[0]
doc = scrape('https://ncode.syosetu.com/'+ncode+'/')

title = doc.xpath('//p[@class="novel_title"]').inner_text
creator = doc.xpath('//div[@class="novel_writername"]').inner_text.gsub(/[\r\n]/,"").gsub(/作者:/,"")
chapters= []
episodes = []

results = doc.xpath('//dl[@class="novel_sublist2"]')
results.each do |r|
  chapter_title = r.xpath('preceding-sibling::div[@class="chapter_title"][1]').inner_text
  subtitle = r.xpath('descendant::a').inner_text
  link = r.xpath('descendant::a/@href').inner_text
  
  chapters.push chapter_title unless chapters.last == chapter_title
  episodes.push [chapters.length-1, subtitle, link]
end

io = Zip::OutputStream.open(File.basename(title)+'.epub')

io.put_next_entry('mimetype')
io.puts 'application/epub+zip'

io.put_next_entry('META-INF/container.xml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="content.opf" media-type="application/oebps-package+xml" />
  </rootfiles>
</container>
EOS

io.put_next_entry('content.opf')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0" xmlns="http://www.idpf.org/2007/opf" xml:lang="ja">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:title>#{title}</dc:title>
    <dc:creator>#{creator}</dc:creator>
    <dc:language>ja</dc:language>
  </metadata>
  <manifest>
    <item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />
    <item id="title" href="title.xhtml" media-type="application/xhtml+xml" />
    <item id="main" href="main.xhtml" media-type="application/xhtml+xml" />
    <item id="style" href="style.css" media-type="text/css" />
  </manifest>
  <spine page-progression-direction="rtl">
    <itemref idref="title" />
    <itemref idref="toc" />
    <itemref idref="main" />
  </spine>
</package>
EOS

io.put_next_entry('toc.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>目次</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
  <nav epub:type="toc">
EOS

if chapters.length > 1
  io.puts '    <h2>目次</h2>'
  io.puts '    <ol>'
  chapters.each_index {|i| io.puts '      <li><a href="main.xhtml#Ch_'+i.to_s+'">'+chapters[i]+'</a></li>'}
  io.puts '    </ol>'
elsif episodes.length > 1
  io.puts '    <h2>目次</h2>'
  io.puts '    <ol>'
  episodes.each {|e| io.puts '      <li><a href="main.xhtml#Ep'+e[2].gsub("/", "_")+'">'+e[1]+'</a></li>'}
  io.puts '    </ol>'
else
  io.puts '    <ol hidden="hidden">'
  io.puts '      <li><a href="main.xhtml">本文</a></li>'
  io.puts '    </ol>'
end

io.puts <<"EOS"
  </nav>
</body>
</html>
EOS

io.put_next_entry('style.css')
io.puts <<"EOS"
html {
-webkit-writing-mode: vertical-rl;
-epub-writing-mode: vertical-rl;
writing-mode: vertical-rl;
}
nav ol { list-style: none; }
EOS

io.put_next_entry('title.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>表題</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<h1>#{title}</h1>
#{creator}
</body>
</html>
EOS

io.put_next_entry('main.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>本文</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
EOS

if episodes.length == 0
  io.puts '<section>'
  puts_novel(io, doc)
  io.puts '</section>'
else
  temp= nil
  episodes.each do |e|
    STDERR.puts e.join(' ')
    
    io.puts '<section>'
    unless temp == e[0]
      io.puts '<h2 id="Ch_'+e[0].to_s+'">'+chapters[e[0]]+'</h2>'
      temp = e[0]
    end
    
    io.puts '<h3 id="Ep'+e[2].gsub('/', '_')+'">'+e[1]+'</h3>'
    puts_novel(io, scrape('https://ncode.syosetu.com'+e[2]))
    io.puts '</section>'
    
    sleep 3
  end
end

io.puts '</body>'
io.puts '</html>'

io.close

今後の課題

  • 画像は表示できない。

メモ

  • 目次はulタグではなく、olタグでなければならない。代わりにCSSでlist-style: none;指定する。
  • Apple Booksはnavプロパティなしだと壊れていると判定する。一方、Kindleは表示できる。
  • 本文中にimgタグが含まれると、kindle端末が表紙と誤解してしまうため、画像を削除している。

キースイッチで、そろそろ動物園ができる。

動物園にしては、パンダが多めである。個人としては、Rhinoceros🦏が登場するのが待ちどおしい。

Linear

- Name Actuation Bottom-out
🐬 Durock Silent Linear Dolphin - 62g
🦛 Kinetic Labs Hippo - 63.5g
🐕 Kinetic Labs Husky - 63.5g
🐼 Invyr Panda - 67g
🦙 Alpaca - 62g
🦙 Silent Alpaca - 62g
🪼 Kailh Jellyfish "Y" 50±10g 60g
🐸 MOMOKA Frog 54g 62g
🦩 MOMOKA Flamingo - 67g
🐍 JWK Moyu Green Snake - 65g
🐈 Glorious Lynx - 60g
🦦 Gateron Baby Raccoon 55±8g -
🦄 Kailh Pink Unicorn 50g 63.5g
🐅 TTC Tiger 45g 55g

Tactile

- Name Actuation Bottom-out
🐼 Drop + Invyr Holy Panda - 67g
🐼 Drop Holy Panda X - 60g
🐼 Glorious Panda - 67g
🐼 FEKER Like Holy Panda 67g -
🐼 TECSEE Purple Panda 55g 67g
🐼 Outemu Panda 50g 65g
🐼 TTC Holy Panda 45±10g 55±10g
🐧 Kinetic Labs Penguin - 63.5g
🐟 Kinetic Labs Salmon - 63.5g
🦘 Gateron Baby Kangaroo - 59g
🦐 Durock Shrimp Silent T1 - 67g
🦈 MOMOKA Shark - -
🐝 GamaKay Bumblebee 45±10g 65g
🔥 GamaKay Phoenix 40±10g 45g

Clicky

- Name Actuation Bottom-out
🦊 Chosfox x Kailh Arctic Fox 52g 53g
🪼 Kailh Jellyfish "X" 47±15g -
🦉 Kailh White Owl 46g 70g

キーボードのハードウェアマクロでExcelの文字を赤色にする

背景

Excelの利点は、マクロ/関数/入力制限/条件付き書式などの機能が利用できることです。これらは技術分野との相性がよいです。一方で、Wordと違って校閲機能がないため、変更点が可視化しにくいです。

Excelで変更点を記録するには、赤字や取り消し線で記録することになります。この操作はマウスを使うのが一般的ですが、キーボードから手が離れるため、作業効率が落ちます。

ショートカットキーもあるのですが、長いため、現実的ではありません。

赤字 Alt,H,F,1,↓,↓,↓,↓,↓,↓,↓,←,←,←,←,ENTER
黒字 Alt,H,F,1,↓,←,←,←,←,ENTER


ソフトウェアでマクロを登録するなどの対策も考えられますが、会社支給パソコンへの任意ソフトウェアのインストールは、禁止されています。

対策

Keychron Q2を購入し、ハードウェアマクロを登録することにしました。VIAではなく、Remapをつかっています。

会社支給パソコンは、キーボードが日本語配列であるため、選択肢が限られます。Keychron Q2は数少ない解決策といえます。

superkopek.jpKeychron Q2 QMK カスタム・メカニカルキーボード ノブバージョン

65%日本語配列キーボード

  • キーボードは小さい方がよい。
  • 十字キーは必要である。
  • Deleteキーは必要である。(60%にはない。)
  • 会社支給パソコンは日本語配列である。

これらを叶えるのが、65%日本語配列キーボードである。

ARCHISS ProgresTouch RETRO TINY

  • 2016年販売
  • Fn+右Shift+5を押してもShift+F5にならない。

archisite.co.jp

Keychron Q2

  • 2022年発売
  • Keychronの公式サイトで販売しているISO配列より、キーが2個多い。
  • ホットスワップ
  • キーマップカスタマイズ可
  • 重く、高い。

superkopek.jpKeychron Q2 QMK カスタム・メカニカルキーボード ノブバージョン

STREAK65 JP

www.ask-corp.jp