Skip to content

sindarin-inc/desktop-app

Repository files navigation

Sol Desktop App

This is the desktop app that supports direct file transfer as its primary purpose, but secondarily, affords us some debugging capabilities and potentially firmware flashing down the road (in a pinch). It requres you to plug your Sol Reader into your computer via USB, though there are companions (noted below) that explore BLE connection as well.

Development notes

From PR #3: Alpha File Uploader. I'm going to merge this PR because after spending some time with these files yesterday and today, I've 95% decided I probably need to declare bankruptcy on much of this code. It's not worth trying to edit these, but it'll be cleaner to rethink the architecture of this app from scratch now that we know what we need in the thing. The good news is this code does work with embedded-app version 1.0.7. We can decide whether we want beta testers with this version or if we want until it's reimplemented and therefore likely way more robust. Here's my analysis:

port_finder.py : in good shape since copied from embedded-app process_epub.py: in pretty good shape since mostly copied from web-app but want @samuelclay to review more closely to concur connect_usb.py: ok-ish, but we need to decide who is responsible for drawing elements and who is responsible for checking USB status. Right now this file does a bit of both. Also, it opens the Serial port every .5 seconds to do its checking, but that is probably unnecessary (and you see that in the logs of the device of the state change). file_transfer.py: total rewrite necessary. I'm conflating a bunch of command processing in various spots that should be refactored out. timing out is inconsistently handled. error checking is inconsistent (and probably fragile as a result). Need to look for opportunities to share code between transfer_put_file and transfer_get_file. direct-gui-qt.py: total rewrite necessary. there are some layout/widget things that need to get cleaned up. but similar to file_transfer.py, the handling of command send and receive isn't consistent. Error handling and response is ad-hoc.

I'll put these notes in the README.md as well.

Current features

The app direct-gui-qt.py can currently restart the device, get device info, and mirror the display. It also can directly upload files. The default functionality is only the file upload capability but by passing --debug to the script, you can enable the other features. Alternately, you can click on the Sol Logo in the window 5 times within 3 seconds to turn on the other features.

Screenshot 2024-08-08 at 8 00 45 PM Screenshot 2024-08-08 at 8 01 07 PM

(With and without debug on)

Another app sol-word.py demonstrates controlling the device via bluetooth (the same way the Sol Mobile app Remote does) and also sends keystrokes to the device via bluetooth. Note that this does not implement auth via pairing so the firmware must have auth disabled for the bluetooth communication to occur. We may wish to have a superuser auth that these things can send to override pairing auth, but we'll need to consider the security ramifications of that. Alternately, we could auth the user in the desktop app by opening up a browser and registering a universal link on each platform so the website can notify the app that the token is ready to be shared. As we don't necesssarily need bluetooth connection now, this is a lower priority.

These are both in prototype form at the moment.

Building into an app for distribution

To build an app from the python scripts, you can find the pyinstaller command in make-app.sh. You will need to ensure pyinstaller is installed on your machine: pip3 install pyinstaller and on Windows ensure that the resulting installation is in your PATH. The output of the command will be in the dist subdirectory.

Commands

On mac, the device enumerates with two ports in /dev -- one is /dev/cu.usbmodemsolreader1 (from hereon referred to as the COMMAND AND TRANSFER port) and the other is dev/cu.usbmodemsolreader3. The latter port is only used for logging when the LOGGING_ON debug command is sent to the device. Any apps wishing to communicate commands and perform file transfers should only use the COMMAND AND TRANSFER port. The device is always listening for commands on the COMMAND AND TRANSFER port. The supported commands in the firmware version 1.0.6 are:

CONNECT Essentially a dummy command that simply echos back CONNECT to the app to establish that the communication is working. This is used by the app to show the user some signal that a file is ready to be uploaded.

RESTART Restart the device. When the command is successfully received, the device will respond over serial with RESTART\n

PROGRAM Put the device in programming mode for flashing firmware. It disables logging and initializes JTAG for programming. It does not respond with an ack (though perhaps it should?).

DEVICE_INFO returns the serial number, the MAC address, and the free space on the filesystem of the device. The format for the return data over serial is:

DEVICE_INFO\n SERIAL_NUMBER, [the actual serial number] ; MAC_ADDDRESS, [the actual mac address] ; FREE_SPACE, [the actual free space]

We likely will want to clean up this aspect of the protocol should we continue using this funcitonality. The direction would probably be to move it to the TRANSFER port where the device sends a json file with all the info that the app can use however it sees fit.

MIRROR When this is enabled, the device will send MIRROR\n and then subsequently send the entire framebuffer over serial on each page refresh chunked in 1024 chunks. It sends this on the TRANSFER port. A second MIRROR command sent to the device will toggle the mirroring off.

In addition to these command, there are other commands for debugging but those are explained elsewhere and not used for the desktop app, hence they are elided in this discussion.

Future:

The next major command to implement is FILE_TRANSFER which will initiate a file to be transferred from the app to the device. For debugging puropses we may wish to implement sending and receiving files from the device (for example, if we wish to interrogate readables.json from the device easily).

Command protocol

The current command protocol for each command is described above. However, it's bespoke and clunky. We should move to standardize this protocol as follows:

A command can have 0 or more arguments. The successful send and receive handshake is:

TX: COMMAND [Arg1] [Arg2] [Arg3] ... \n
RX: COMMAND [Arg1] [Arg2] [Arg3] ... \n
...

All arguments are passed after the command with a terminating \n to indicate the end of command string. When the receiver echos the command string with arguments, the sender can assume the command has been executed or initiated (in the case of file transfers). For example, for RESTART:

TX: RESTART
RX: RESTART

The app knows at this point the device is restarting. The sender can issue the VERSION command to the device -- this returns the firmware version on the device so we can use that as a map to the current protocol that is implemented on the device.

Error handling

If at any point in the handshake, the device sends back ERROR, the app should assume that none of the command has been processed and should start over from the top level command.

Additionally, the app should implement a timeout when waiting for a response from the device. If the device does not respond within 3(?) seconds, it should be assumed an error has occured with the command. On the device, we will attempt to do similar error checking and in the case where an error state happens, the device will restart to try to remediate the problem. The device will also implement a timeout such that if it expects to get more information from the app (e.g., an argument, or its own ack in the handshake), it will send ERROR if it doesn't receive a response in time.

File transfer for EPUBs

To transfer EPUBs from an app to device, we define a general File Transfer Protocol and then on top of that, define the command for transferring an EPUB.

The app is responsible for processing EPUBs before sending them to the device.

A note on processing EPUBs. From an architecutre standpoint, since we will now be processing EPUBs on the server as well as in the desktop app, we recognize that there will be two codebases doing processing. Rather than try to consolidate the codebases into one, given how small and light the processing code is, we'll simply duplicate it and not worry if they get out of sync slightly, as long as both on the cloud and on the desktop app, we verify that the proccessed EPUBs are valid. One way to mitigate potential errors and for tracking puropses is to add a processed_by field to the entry in the readables.json that includes whether the desktop app did the processing or the cloud did. We may also include a version of the app in case we need to track down a problem with a processed epub.

The app is responsible for all the same things the cloud is in processing an epub and sending it to the device. This means it should not only process the EPUB, but it should generate a json entry that can be added to the readables.json file. In addition to the usual descriptors found in that file, an entry indiciating local should be included so the device knows the file was directly upoaded and wasn't synced from the cloud. At this time, we will not sync local EPUBs back to the cloud but we should still do all the telemetry. On the device, we'll provide an option to turn off reading telemetry generally (but probably not with the granulatity of local versus cloud).

Generic File Transfer Protocol

The FILE command initiates a file transfer. It has four arguments: The first argument is PUT or GET. For PUT, the second argument is the file type (currently either JSON or EPUB). For GET, the second argument is the file name to fetch from the device.

For example, on the COMMAND AND TRANSFER port:

TX: FILE PUT JSON 987 a1c2e3
RX: FILE PUT JSON 987 a1c2e3

This will initiate a file PUT on the COMMAND AND TRANSFER port after the receiver sends the acknowledgment. Once that transfer is complete and the receiver has verified the checksum, the receiver will send a last acknowledgement:

RX: FILE PUT [SUCCESS|ERROR]
TX: FILE PUT SUCCESS

to indicate the file has been successfully received or an error occured and the send must be retried. The sender will then ack the FILE PUT SUCCESS to complete the handshake so both sides know the file was PUT successfully. Similarly, for GET

TX: FILE GET /readables.json
RX: FILE GET /readables.json 987 243

In this case, for the ACK of the command, the receiver will respond with the command but also additional payload indicating the size of the file and the hash. After this exchange the device will send the file /readables.json on the COMMAND AND TRANSFER port immediately after sending the ack of the command.

If the file isn't found, the device will send back ERROR. At this point, we won't worry about error codes, but we could easily add that in the future as an arg that is always passed back when an error occurs. The file transfer will then commence on the COMMAND AND TRANSFER port.

RX: [file chunk 1]
RX: [file chunk 2]
RX: [file chunk 3]
...

Note that we will always chunk the file and the chunk size will be 1024. Note, we also chunk the stream that comes from the MIRROR command similarly. We may add dynamic chunk sizes down the road that the device and app can negotiate via handshake, but that seems unnecessary now.

In our testing, we're getting ~50k/second transfer speed. We'd like to verify that the connection is still solid every ~500ms in case an interruption occurs. So for transfers more than 25k, every 25 chunks, we require an ack on the COMMAND AND TRANSFER port that the connection is still live. The ack looks like this for the FILE GET command:

TX: [CONTINUE|CANCEL]
RX: [CONTINUE|CANCEL]

Either side can send a CANCEL at this point to stop the transfer. If a transfer is stopped, the both app and device should assume it must be started again from the top level command. If both app and device send CONTINUE then the next 25k chunks will be sent. Note that for the last set of chunks (25 or fewer), there will not be an ack -- just the following end of file ack.

One the file has been recevied, the app will check the received file's checksum against the checksum provided in the handshake. If it's the same, the app will respond on the COMMAND AND TRANSFER port:

TX: FILE GET SUCCESS

And if it's different:

TX: FILE GET ERROR

For the FILE PUT command all of the above is the same with the exception of the handshake for checking in every 25 chunks. We add an extra ack since the receiver should initiate the ack after receiving the 25th chunk:

RX: [CONTINUE|CANCEL]
TX: [CONTINUE|CANCEL]
RX: [CONTINUE|CANCEL]

This is to keep continuity from other commands where before the app starts sending, it receieves the go-ahead from the device as the last part of the handshake. This might be overkill, but seems more intuitive than having this be the one exception to the other cases.

Sending an EPUB

To encapsulate a unit of work for sending multiple files, we also implment the TRANSFER command. It takes a single argument indicating the type of transfer (plus any additional arguments required by the type). Both sender and receiver are responsible for knowing the protocols for transferring different types of things like books. Currently, the only implemented TRANSFER subcommand is BOOK.

TX: TRANSFER BOOK [UUID]
RX: TRANSFER BOOK [UUID]

The UUID argument is optional as it is also found in the json for the book entry. Once the receiver has acknowledged the command, the sender can proceed to send the json and epub for the book using the generic transfer protocol above:

TX: FILE PUT JSON 987 a1c2e3
RX: FILE PUT JSON 987 a1c2e3
[chunked json file sent on TRANSFER port]
RX: FILE PUT SUCCESS
TX: FILE PUT SUCCESS
TX: FILE PUT EPUB 432321 f4d3c2
RX: FILE PUT EPUB 432321 f4d3c2
[First 25 chunks of EPUB file sent on TRANSFER port]
RX: CONTINUE
TX: CONTINUE
RX: CONTINUE
[Second 25 chunks of EPUB file sent on TRANSFER port]
RX: CONTINUE
TX: CONTINUE
RX: CONTINUE
...
RX: FILE PUT SUCCESS
TX: FILE PUT SUCCESS
RX: TRANSFER BOOK [SUCCESS|ERROR]
[device restarts if SUCCESS]

After the two files are received, the EPUB file checksum has been verified, and the device has successfully added the EPUB to the local storage (including updating the readables.json), the device will send a TRANSFER BOOK SUCCESS command back to the sender. The sender should the recognize that the device will restart as part of this protocol since the menus need to be updated. Any serial connection to the device must be re-established after the TRANSFER BOOK successfully completes.

Note that this command definition assumes the device knows where to store the incoming files on the device (the json being part of readables.json and the EPUB being stored in somewhere in /books/). For simplicity on the app side, the app is completely blind to filenames of these files and where they will be stored.

About

Desktop app for the Sol Reader

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors