A few days ago, I decided to do something completely pointless. What if I could broadcast to the world on Discord what book I'm reading? The only hurdle was... I read books on my e-reader, so... well... "that won't be easy to get data off of and onto Discord." (Now... I know it's pointless. It was really just Rust practice.)

Program Structure

Before I did anything (we could consider this the 0th step), I came up with the general system for how my program would work. Since the RPC needs to be sent from my computer, I'd need two parts: a server, and the program on my e-reader. I decided to have my server use HTTP, and I would send a request from the e-reader. I'd need to run this program on the e-reader every so often, so I thought I'd use a cron entry to run it on a certain interval.

Getting Data from the Kobo

This was the first step to acheiving the goal. Effectively, I wanted to get some specific information from the e-reader, namely the page I was on, the author, title, the total number of pages, and such. Initially, I thought this would be a huge hurdle, since I would have to somehow pull this data from an application that I knew virtually nothing about (which is Nickel, the Kobo user interface (or at least I think that's the correct name for it)).

Actually, it wasn't that hard. I had a sudden flash of memory: the Kobo uses an SQLite database to store its informations and such, and that database is conveniently in /mount/onboard, which is what the Kobo exposes when you connect it via USB to your computer (albeit in a hidden folder). Step one: copy that database, called KoboReader.sqlite to my computer for some analysis. There was a second sqlite file, but my computer didn't recognize it as an SQLite database (the icon was different), and trying to read it rendered no results, so I just used KoboReader.sqlite.

Next, I loaded it into DataGrip. Why DataGrip...? Well, I'm sure I could have just used the command line to figure stuff out, but I'm lazy, and I had rather been wanting an excuse to use DataGrip for some time. Also... I don't really know the SQLite command line (the only SQL database I'm really familiar with is MySQL), so this was easier. So, I loaded it in... and was met with a gigantic list of tables in the schema. There are a lot of tables in the Kobo database, most of them are empty, and the only one that I found was important was content. The content table seems to store things about books in the database, although a lot of it is very random and it seems like the developers just kept adding to the same table so it serves multiple purposes now. So much for migrations (although I can feel the pain of that, migrations in my basically-from-scratch Flask application emotes were... difficult).

In the content datbase, I could find many things... but something that was not included was the page number. Instead, I'm pretty sure the e-reader stores some kind of tag-location that resembles something like a CSS selector: .html_split_025#point(/1/4/2/2/2/1:0) (which is in the ChapterIDBookmarked column, so I guess that makes sense). Anyway, I doubted that anyone would be able to figure out anything from that piece of information, so I moved on from it. Instead, I focused my sights on getting the author, which was in the Attribution column, the title, which was in the Title column, and... well, I didn't think about it in the beginning, but the database also had a column for the time read so far, in seconds, which was the TimeSpentReading column.

I still wanted the chapter name, though. It turns out that's also in the content table. Now, just a quick note. I previously mentioned that it seemed that the Kobo developers used the same table for more than one purpose. This means that in some entries, the BookTitle column was the actual title of the book, and in some other cases, the Title column was the title of the book. For those columns with the BookTitle, I found that Title would represent chapter names. Now, in my initial query, I sorted by DateLastRead, which is a column I haven't mentioned before but hopefully it's fairly self-explanatory. The one row I got in my query (I did add a limit 1) was a Title row. Now, with all this information, how was I supposed to get the chapter from Title? The answer lied in another field called adobe_location, which always had the first part of that weird CSS selector thing, before the pound sign.

Here's an example: (Sidenote: the book I specified in BookTitle is a rather good read in my opinion :) sqlite> select adobe_location, Title from content where BookTitle='The Way of Kings'

.html_split_010 | Prelude to the Stormlight Archive
.html_split_011|Book One: The Way of Kings
.html_split_013|Prologue: To Kill
.html_split_014|Part One: Above Silence
.html_split_015|1: STORMBLESSED
.html_split_016|2: HONOR IS DEAD
.html_split_017|3: CITY OF BELLS

So, basically, to get the chapter, I just had to do something like ChapterIDBookmarked.split('#')[0] and feed that into a where with adobe_location. Now that I knew how to extract all this information from the database, I set about writing a program to do it for me.

Automating Collecting the Data

I already knew I was going to write the program running on the Kobo in Rust, just to practice. In actuality, I found out later that writing this in something else like Python would have just proved a nuisiance in terms of deployment. I didn't have a clue on how I'd be getting the program to run on the Kobo, and I didn't want to mess with its database just yet, so I used my copied database and decided to develop the program first on my local computer.

It wasn't hard. You can see the code on my GitHub repo, which will be posted soon (and this blog updated). Basically, it just runs a few SQL queries, gets the data, and puts it into a struct. For additional safety, I made the program copy the database to the local directory so we're not operating on the original, and delete it at the end. Once that worked, I set out truly hacking my Kobo.

Getting Shell Access on the Kobo

When I first started this, I had no clue what I was doing, nor how to do what I wanted. All I knew was that I wanted SSH access to my Kobo, so I could update the crontab, and run my program. Searching for "SSH to Kobo" and other variants of those keywords rendered no result. Eventually, I found this, which is a library to print text to an e-reader's screen. After looking around, I found that the developer of this tool said he bundled an SSH server with this. Although... I was skeptical. I also ended up reading somewhere else FBInk was already included with NiLuJe's other tools, which would mean it would be included with kfmon, which I wanted to install since I had no clue what I was doing, and kfmon had instructions on how to install it.

So, I did that. It added some new entries to my library, such as a functionally useless Plato and KOReader entry. Actually, delving a little deeper into the documentation of kfmon, it seemed as if it were used to launch applications, from the Kobo "homescreen" (there's definitely a "more correct" word for this, but I'm not sure of it). So... well, I had also seen this other tool called nickelmenu, which... I saw go hand-in-hand with "kfmon?" I wasn't really sure what its purpose was, or if it would give me remote access to my Kobo, but I decided to try it out nonetheless. Upon installing it, well, it doesn't do anything out of the box. Basically, it adds this little menu to the bottom of the screen on the Kobo. If you configure NickelMenu, though, it adds entries to that menu. Well... what kinds of entries? Looking at the configuration documentation, well, one of the examples was turning on a telnet server, which does exactly what I want! So, I copied the example code into my Kobo, et voilà! I had the option to enable a Telnet server on my Kobo.

Actually, it worked as well. Funnily enough, the security on the Kobo is pretty bad; the username to log in is just 'root' and the password is blank. And... I was into the Kobo! I explored a little bit, and it's just a Linux system. It even has vi (I'll assume that this wasn't because of one of the other things I installed on my Kobo in vain before NickelMenu like KoboTools)! Anyway, now that I had gotten access to the Kobo, I could move on to the next stage... although I found that my Kobo seemed to be disconnecting from its Wi-Fi more often, causing my telnet sessions to freeze. I had to repeatedly go back and reconnect the Wi-Fi manually, and hope it didn't disconnect after 30 seconds.

Running Repeated Tasks on the Kobo

I had already written my program at this point, and just needed to port it to the Kobo, so I decided to work on something else before cross-compiling my program. The question that I needed to answer here was, well, even if I had a program, how was I going to get it to run every so often? It wouldn't be very worthwhile to have my Rust program run from kfmon and have to go to the home screen just to update my status. I mentioned before that I had wanted to use crontab, but... well, the Kobo doesn't have a crontab. I almost gave up at this point, since there'd be no point if I couldn't repeatedly run my task... until I made a surprising realization.

My realization was that udev exists. More specifically, udev rules exist, and I saw how the Kobo was already seemingly using some udev events to run programs. I realized well, I could emulate that and hook into some reocurring event to get my program to run. With a simple udevadm monitor, which lists events that udev sees in the terminal. Among those events included events for plugging/unplugging USBs... and.... about every minute or so, battery change events. I whipped up a little udev rule for my Kobo that added to a file in the /media/onboard directory, which is the directory that the USB mount exposes. After a little while, and a little tweaking on the udev rule (I think it was running twice because my rule matched both events from KERNEL and UDEV events, if that's what they're called)

Well, since I could get this little echo to a file to work, I could definitely also run my program every so often. I'd just have to change my udev rules and reload. With that out of the way, I could move on.

Writing the Server

I thought I'd take a break and write my server, since I'd have to integrate this into the client anyway before I ran the final program. It seems since Discord deprecated their RPC api in favor of something else (called the GameSDK?), many library RPC library developers deprecated their libraries. Nonetheless, I found one that still existed for Python (although I wouldn't mind changing the language to Ruby or something later for fun), called pypresence.

Again, this program was fairly simple. I just used Flask to listen for requests at the url /update, which grabbed a bunch of prameters and used it to update my RPC "game." After testing it out manually, and fixing up any issues, it worked pretty well. Actually, I was under the assumption that I could use as many lines as I wanted for my game information, but it turns out I only had three, so I had to get creative with my formatting to get everything I wanted to fit.

Cross-Compiling the Client Program

With everything else set, before I got the client program to actually make my HTTP request, I just wanted to run it on the Kobo and make sure that worked. After combing a few places, I found out that basically, I had to add a new toolchain to Rust, the arm-unknown-linux-gnueabihf one, and download this file from GitHub, which contained a gcc that I had to use for the Kobo. I then added that gcc to my path, and updated a file called ~/.cargo/config to tell Rust what... linker to use. My file looked like this:

[target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

Now, when compiling, I just had to use the command cargo build --release --target=arm-unknown-linux-gnueabihf. I could look in the target folder to find my binary. But first, I had an issue with the sqlite library I was using with Rust, rusqlite. I don't remember the exact error, but it effectively failed to compile. I ended up having to enable the bundled feature for the crate for that to compile.

Once I got it to compile, I made Rust explicitly make a copy of the sqlite database (and delete it at the end) so we were operating on something that wasn't the orignial; it felt a bit risky to be doing that. After testing that and seeing it worked, I was approaching that Moment of Truth™. Would it work? Well, after updating the original database location in the code to /mnt/onboard/.kobo/KoboReader.sqlite, and recompiling... I copied the file over to the Kobo via USB, and connected via Telnet. At this point, all the program did was print out the data it had loaded into the struct I created earlier. I ran it... and it worked.

Updating the Client to be a Client

At this point, the client... well, didn't actually send any HTTP requests. It was time to fix that. I did that with the synchronous verson of the create reqwest, which is great for simple programs like these. Actually, I haven't learned how to do asynchronous things in Rust yet, but I don't think it's particularly necessary in this situation either. Either way, reqwest, coupled with the very helpful serde crate automatically converted my struct into a dictionary, allowing me to do my HTTP request from Rust in a mere five lines (although that could have been made into even less, probably).

Testing it out from my computer, it worked. Then... I tried to cross-compile it, and I got more annoying errors... because of reqwest's dependence on OpenSSL. Since I had a vague idea that OpenSSL would have to be cross-compiled as well, or something, and I didn't want to do that, I looked around in terms of reqwest's features, and found that I could change the TLS library I was using. First, I tried the rustls-tls option, but it failed to compile because of certain CPU features missing on the ARM CPUs, I think. Eventually, all was saved, and the native-tls-vendored option seemed to compile. Crossing my fingers, I copied my binary onto the Kobo and telnet-ed in... to find it didn't work because I forgot to update the SQLite database path. Finally, after doing that, and keeping my simplistic Flask server running, my Discord status was updating. There was only one last step left!

Finishing Up/Conclusion

After updating that udev rule to use my Rust program instead of echo, all I had to do now was to hope that it worked! I set off to reading, having Discord and my server running at the same time.

For the first three minutes, all was well, and then my status stopped updating. In the end, that was fine, since the Kobo didn't really update the database until the end of each chapter (which is especially fine in my case, since in the book I'm reading, the chapters are really long). Actually, the lack of a status update was caused by the Wi-Fi issues I was having earlier. I'm not sure if this is something I simply didn't notice earlier, or something I caused by installing stuff I probably didn't need on my Kobo when trying to get remote access. A reboot didn't really fix this, either. Effectively, I found the best way to "force-update" my status was to search a random word in the book on Google or Wikipedia, which makes the Kobo connect to the internet. I also tried explicitly enabling internet from random commands I found in the udev rule "hook," which didn't really help. The current situation is fine for now.

I just added an autostarting systemd rule for my server. The server still needs a little bit of fixing. For example, I'd like it to reconnect to Discord, because I don't have Discord open all the time, and the only way to mitigate this right now is to explicitly restart my server (systemctl restart --user ereader).

Nonetheless, I'd say this project was a success. Obviously, it could be polished a bit, but whatever. It works, and I didn't really think I would actually fullfil my idea... until I did.