Integrating a Kindle into house automation2018-12-15 [ iot ]
Most nights I read from my Kindle until fall asleep, leaving my night light on.. after a few hours I usually wake up, annoyed at the light and turn it off, but some nights I cannot fall back asleep..
So, I thought why not have the light turn itself off when I fall asleep? Given that I can already turn it on/off via my home automation scripts, it should not be too difficult to detect somehow that my kindle went off.
The userland on a default jailbroken kindle is quite lmited: You basically get bash, coreutils and some custom amazon binaries.
With these limitations I thought of some ways to detect the screen changes in order of least to most involved:
- On the network (somehow)
- With the provided userland
- With some extra binaries (which? how do I get them there?)
First attempt: Network detection
Initially I thought about detecting the device based on activity:
- Get the IP address when the kindle requests a DHCP lease
- Detect "activity" on the network coming from this device.
The problem with this is that the kindle itself does not do much when you are reading; so I was bound to having a
ping running constantly on device.
Even after having this working, it was not very useful as the kindle will:
- Keep wifi on for a little bit after the screen is turned off
- Randomly turn on wifi
I started by detecting the connect/disconnect events from the kindle on my own OpenWRT AP but the random (and delayed) events from the kindle's wifi were not useful.
root@OpenWrt:~# hostapd_cli -i wlan0 -a /root/script.sh f4:60:e2:b4:68:c4 disconnected on wlan0 f4:60:e2:b4:68:c4 connected on wlan0
Second attempt: Tailing logs in Bash
I started exploring the device's userland and I found that I could somewhat reliably detect the screen changing state by
[root@kindle root]# grep SCREEN /var/log/messages 181215:164756 powerd: I def:statech:prev=ACTIVE, next=SCREEN SAVER:State change: ACTIVE -> SCREEN SAVER 181215:164856 powerd: I def:statech:prev=SCREEN SAVER, next=READY TO SUSPEND:State change: SCREEN SAVER -> READY TO SUSPEND
My initial working attempt looked something like
grep SCREEN /var/log/messages | while read -r line; do echo $line | nc -u 192.168.2.10 8888 done
and I simply ran it in the background from an ssh session.
This attempt was a bit crude; as it did not reliably manage to send "SCREEN ON" messages, the wifi shuts off while the system is sleeping, and this will try to deliver the message before the wifi connection is up.
I did try to periodically check whether the wifi was down and queue the message but it got quite cumbersome to deal with in bash, which led to..
Third attempt: Python
I thought, if this is just an ARM linux distro, could I not just
scp over some python binaries and be done with it? Well, yes!
[root@kindle root]# file /mnt/us/python/bin/python2.7 python2.7: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 2.6.31, stripped
I originally got the binaries from (somewhere?) in the Debian repos, but you can also find them here.
Now that I had a handy python binary in my kindle I could more comfortably manage the state required to send messages only when appropiate, so I essentially ported my bash script to python, but I did not like the (working) result.
I decided to dig a bit deeper to avoid the fragile parsing of
/var/log/messages and I found
lipc-wait-event (you can find a detailed list of events here).
Investigating the events, I left
lipc-wait-event running for a bit while I played with the kindle
[root@kindle root]# lipc-wait-event -mt "com.lab126.powerd" "*" [11:28:12.711741] goingToScreenSaver 2 # on button click to power off [11:28:17.554321] t1TimerReset [11:28:17.580866] outOfScreenSaver 1 # on button click to power on [11:28:24.081409] goingToScreenSaver 2 [11:28:27.298605] outOfScreenSaver 1 [11:29:27.421299] charging # when a charger was plugged in [11:30:20.656191] battLevelChanged 21 # after waiting for a bit
[root@kindle root]# lipc-wait-event -m com.lab126.wifid "*" scanning # enabled airplane mode scanComplete cmDisconnected cmStateChange "NA" signalStrength "0/5" cmStateChange "READY" cmIntfNotAvailable cmStateChange "NA" cmStateChange "PENDING" # disabled airplane mode scanning scanComplete cmConnected cmStateChange "CONNECTED" signalStrength "4/5" scanning cmConnected cmConnected scanComplete
I could now subscribe to these two event sources individually and get reliably notified when something interesting happened.
My final script:
#!/usr/bin/env python2 import subprocess import socket import time from threading import Thread client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_socket.settimeout(1.0) def udp_send(key, value): try: print("Sending", key, value) message = b'%s|%s' % (key, value) addr = ("192.168.2.1", 12000) client_socket.sendto(message, addr) except Exception as e: print(e) def pub_event(source, events): comm = ['lipc-wait-event', '-m', source] comm.extend(events) s = subprocess.Popen(comm, stdout=subprocess.PIPE) for line in iter(s.stdout.readline, ""): line = line.strip() print(source, line) if 'goingToScreenSaver' in line: udp_send('screen', 'off') if 'outOfScreenSaver' in line: udp_send('screen', 'on') if 'battLevelChanged' in line: udp_send('bat', line.split(" ")) if 'cmConnected' in line: udp_send('wifi', 'on') udp_send('init', 'init') power = Thread(target=pub_event, args=("com.lab126.powerd", ["*"])) wifi = Thread(target=pub_event, args=("com.lab126.wifid", ["cmConnected"])) power.start() wifi.start() power.join() wifi.join()
On the other end there's a very simple udp server that bridges the data to mqtt.
There are quite some ways to add "services" to the kindle, but I opted for an upstart entry:
# Script run after all filesystems have loaded start on started filesystems_userstore stop on stopping filesystems export LANG LC_ALL pre-start script python /mnt/us/test.py & end script
Doing remote work on the kindle is quite annyoing as it will try to limit its bandwidth and shut down the wifi at any opportunity; setting the wlan power setting to
maxperf solves the issue
$ cat /mnt/us/fast_wifi.sh wmiconfig -i wlan0 --power maxperf
Don't forget to set it back to
rec later, or your battery will drain quite quickly.