Thursday, January 18, 2024

Reversing the Web-@nywhere Watch: browse fragments of the Web on your wrist

In the halcyon days of analogue modems and POTS dialup Internet, when the only wireless connection in your house was between the cordless phone and the wall, anything having to do with the Web was best consumed in small bites (pun intended). If you wanted to take data with you, you downloaded it first.

Which brings us to this.

Smartwatches at the turn of the century were a more motley assortment than today's, with an even wilder range of functionality. If you had a few hundred dollars or so, there were some interesting options, even back then. But if all you had was $85 (in 2024 dollars about $150), you still weren't left out, because in 2001 you could get the Web-@nywhere (the "Worldwide Web Watch"). Load up the software on your PC and slap it in its little docking station, and you could slurp down about 93K of precious Web data to scroll on the 59x16 screen — 10 characters by 2 characters — to read any time you wanted!

That is, of course, if the remote host the watch's Windows 9x-based client accessed were still up, on which it depended for virtually anything to download and install. Well, I want 95,488 bytes of old smartwatch tiny screen Web on my wrist, darn it. We're going to reverse-engineer this sucker and write our own system using real live modern Web data. So there!

This watch was not a device I owned back in the day (or had even heard of), and I don't remember what I was looking for on eBay that came up with it, but a couple months or so ago I spotted someone selling this bizarre little watch for (as usual) a typically unreasonable price. A quick check turned up quite a few others, making it more common than I'd thought. Naturally I didn't snap that particular one up, but it sounded like a wacky project to undertake, and I was able to find a new old stock unit a few weeks ago still in the clamshell plastic for substantially less.

It came with all its accessories, including the watch itself, a terrible nylon watch band, a software CD and manual, a lanyard and clip, a pleather vinyl strap with a carabiner and a little watch docking station which connects to a regular RS-232 DE-9 port. The battery was long dead, but everything otherwise looked new.

The history on this thing is rather obscure. The Web-@nywhere Watch, which I'm going to write as Web Anywhere to prevent me from going mad, appears to have originated in Hong Kong based on the echoes of its website on the Wayback Machine (watch out for popup windows). Although the about page refers to a "World Network Ltd." as its corporate seller, the "recruitment" section refers to a www.kinger.com.hk, which seems to be its parent company. Kinger International's own defunct site has an old Flash SWF that Ruffle doesn't like much, but it also triggers a popup window (grr) which links back to ... World Network Ltd.

The World Network Ltd. page indeed features the Web Anywhere watch ("It Is Not Just A Watch !"), but incredibly two other related products as well, a keychain organizer that looks like a watch microcontroller trying to be a tiny Palm V, and what appears to be the ancestor or at least older sibling of the Web Anywhere, the CyberX Watch. The CyberX has something like a 30x16 display and a "PC Connect" that could store "Personal Data and E-mail".

This closeup from a defunct Etsy listing shows some curious features on the CyberX's watch face, even including music fields (singer and song/ranking?!). These apparently came from its own client and docking station, which in addition to synchronizing PIM entries could also send very limited amounts of travel, sports and entertainment "data" presumably predigested for its exceptionally constricted viewport. The watch connected to the PC using three pogo pins on its own RS-232 docking station and synced with Windows-based software. These links are good evidence the CyberX was actually sold — at least to some extent — in Europe by Zeon Ltd., a British subsidiary of Hong Kong Herald Group (apparently unrelated to Kinger).

While the CyberX may well have been a decent databank watch for phone numbers and such, it would have been impossible to consume any Internet content of substance with a screen that small and memory apparently that limited. In addition, the 1998 Seiko Ruputer watch, later sold as the onHandPC by Matsucom, had attracted a lot of attention with its 16-bit Panasonic MN10200 CPU, 102x64 screen and "joystick" control. It was a real computer on your wrist that you could program and load software on in a way that the cheapo pretender CyberX just wasn't. (By the way, that CPU turns up in many other interesting places, including Taito arcade games and the Nintendo GameCube's optical drive.) But what the Ruputer didn't have, at least initially, was a way of directly getting Internet content onto the device (later on there was HandySurf), and I suspect Kinger saw that as an opening. After all, they'd already done it for the CyberX, more or (mostly) less.

I mention the Ruputer specifically because there's no way you'd get a watch looking like this by coincidence, complete with a little Ruputer-esque four-way joystick, unless it was designed to be a direct ripoff (and of course the Ruputer also had three pogo pins in its own docking station too). Even the screen is wide like the Ruputer's, though it's not quite tall enough. It's almost painfully obvious where Kinger got their design cues from.

The rest of the Web Anywhere was an iteration on the CyberX, including a similar built-in Indiglo-style electroluminescent backlight and serial docking station, though the pogo sockets are exposed on the Web Anywhere's watch body instead of under a cover. The two watches also appear to use similar straps.

Since the software alleges itself to be Windows 2000-compatible, the device can't be much older than that, and most likely came out around 2001 based on the earliest entries in the Wayback Machine and other date cues we'll demonstrate. I can't find any record that the CyberX was ever sold by that name or any other in the United States, let alone that horrid little PDA, but the Web Anywhere's local importer was right here in sunny Southern California in western Los Angeles. M&M Watch Company (the timepieces that melt in your mouth, not in your hands?) doesn't appear to have had much of a web site, but this directory entry says they sold "wholesale watches, novelty watches, logo watches, and collectible giftware." Their former address in Calabasas is now currently occupied by a roofing industry organization.

Ultimate sales totals notwithstanding, it does look like M&M imported the watch in some numbers and even today they're not difficult to find. Nevertheless, even though M&M were the local sellers, they didn't seem to do too much in the way of promotion and almost all the marketing was from Kinger, including this cringeworthy synthetic Chinese booth girl type that looks like the result of Midjourney having a stroke. She served as the product mascot and was even available in downloadable background images like this one, in which she wears the watch she's supposed to be avidly using upside down.

Similarly, none of the infrastructure supporting the Web Anywhere Watch appears to have been hosted by M&M locally; it was all overseas on Kinger's dime. We'll get to that a little later.

The Web Anywhere claims to do a lot more than the CyberX. Besides time and date, it also included an address book (phone and E-mail), scheduler, data browser (this is where downloaded content would go), a world clock, a daily alarm, a countdown timer, a "game" mode, a stop watch/basic chronograph with lap function, data link and "graphic animations." The data browser "can access important web links and information." The manual divides the clock into home time and world time modes and mentions reminder features for the scheduler ("planner remind") and up to three "special days" you can count down to.

The battery is good and dead in this, so we'll remove the back cover (I assume the "09/01" printed on its tag was a production date) and swap it out. The watch is decidedly not water-resistant, and you can also see where the vinyl loops for the watchband are degenerating with age despite this unit having been unopened.
The battery is a regular 3V CR2032. To get it out, use a jeweler's Phillips-head screwdriver to remove the small retaining screw on the bottom metal tab, then carefully (the tip of a nylon spudger would be best to avoid scraping the circuit board) unclip the right side out from under those plastic retaining hooks and flip over the battery door. Reverse the steps after replacement and press the RESET button at (in this view) the top right after "replaced battery" [sic] before putting back on the cover.
No Y2K problem with this watch (and the default 2000 year is more reason to peg its introductory date around that time, though it can handle years between 1990 and 2089). The LCD is nice and sharp, the plastic protective sheet is still on the screen and the EL backlight is bright throughout. This thing has indeed never been used.

The buttons are clearly labeled, left to right and top to bottom, EL for the backlight, and then function, mode and del(ete) buttons. However, the rather lengthy Engrish manual bafflingly calls them "Key E, A, B and C" respectively, ignoring their labelling completely. And what's key D? Why, it's (naturally) the blue ENTER key, and keys 1, 2, 3 and 4 are (of course) right, down, left and up on the joystick. That won't be confusing at all!

The interface on this watch was also clearly designed by several committees that didn't talk to each other, or if they did, did so over hallucinogens and/or hostilities. For example, pressing the ENTER button on the primary time screen ... starts up animations. Exactly what you'd guess it would do. Similarly, you cycle through the options for the top row such as date formats with left and right on the joystick, not the function button or up/down. None of this is in any way discoverable except by accident.

The function button at the top right goes through global options, such as configuring your choice of (scratchy) voice alarms or music alarms, as well as feature-specific options when in the other modes. You can also turn the watch display completely off from this screen, which is great for saving battery life.
The mode button at the middle right goes through the watch features, more or less as described on the back of the clamshell package, and I'm not going to go through all of them here (this contemporary review covers the features in a bit more detail). Each of them shows a little animated icon such as these exploding balloons for the game option. However, Kinger's sadistic HIG committees reared their ugly heads here again too: you have to go through all of them over again if you miss the one you want (the joystick doesn't let you back up), and if you linger too long it goes back to the default time screen.
That said, the game option is tantalizing. It gives you three slots to load games, selected by the joystick and ENTER button, none of which come loaded by default — meaning you have to load them on the watch yourself. This also means we could potentially write software for this thing. Take that, Seiko!
But what we came here to see is the "data browser," or what the watch simply calls the browser.
When we select it, it displays the number of bytes free. 93K seems like a lot! I mean, you could fit some pathetic fragment of Google's JavaScript in that ...
However, trying to scroll through it yields only an error.
It's fairly clear we're going to need to get synching working if we're going to get anything interesting working. However, don't bother opening the dock to see what's there because it will likely break the screw races like mine did. The only circuit board inside is a little one to show a red LED when the serial port it's connected to is "live" (i.e., held high in the default state ready for transmission; see this article's primer on RS-232 if you're new to serial links). Only three pins are brought out, namely ground, receive and transmit. The pogo pins are an imperfect fit, even with the button to retract and engage them, and I had to occasionally wiggle the watch around a bit to get a good connection.
I clipped on the carabiner and strap, since that looked nice, slapped it in its docking station, and turned to installing the software. (However, after this picture was taken, the vinyl strap started cracking like the wristband loops, again entirely from age.)
The software comes on CD and includes a melody editor, multiple "interactive manuals" and the "interface program," which is the main client. The disc is labeled "Web-@nywhere V1.0M for Windows 95/98."
Long-time readers will know we never send a PC to do a Mac's job. We'll fire it up in the Virtual PC Windows 98 instance on the Quad G5 and have a look around.
The contents of the CD are very spare, just the installer packs and the executable itself. Everything is dated mid-2001, consistent with our presumed timeframe.
Running the standard installer.
I noticed it dropping Borland runtime files, so we have a good idea what it was written in. (In fact, there are explicit Borland Delphi references in strings within its main executable, making Object Pascal the most likely culprit.) Also notice the "2001" in the lower right corner.
After rebooting the Windows instance to consummate the installation, we look at what it dropped. Most files hail from mid-2000 with the latest being webv10.exe itself, the main executable, dated 28 August 2001. The earliest file I could find was dated 22 May 2000, making it very unlikely the Web Anywhere Watch predated that modification time.
The interactive manuals come in two flavours. A set of Macromedia Flash-generated executables simply show a tabbed click map of the watch and interface. These aren't too interesting and largely recapitulate the manual and its attendant flaws, but the second set are animated screencasts with voice narration generated by Lotus ScreenCam 97 (notice the registration to "kinger" [sic]). Despite my low expectations, their female announcer is fairly articulate and easy to understand. I don't know why she wasn't asked to write the print manual; she might have done a better job on it.
The screencasts are particularly interesting because they show — at least in some idealised fashion — the user-facing process of how the watch received online content. The docking station serves a second purpose besides synchronization: a short product key on a sticker on the bottom acted as log-in credentials to the Web Anywhere Watch website, and before you could do anything else, you had to click the "globe" icon on the toolbar and enter this key into the client (but it calls it a "CD Key" despite the key being nowhere on disc). Incidentally, if the recorded machine's clock is at all accurate, I salute this unknown person for doing her recording at 4am. That's what I call true dedication to the company.
Having done so, a browser will open to the Web Anywhere website, which in this screencast seems to be an internal mockup on a private network. The breadth of available data appears to largely have been travel related information, which would be useful out of your home country where international data roaming might have been expensive or unavailable at the time. The announcer chooses weather information.
Weather information did not comprehensively cover the globe, but did cover many countries down to the city level. The announcer chooses London, UK.
The weather data received wasn't live in this example but rather monthly averages which show up as separate records within the watch's Browser app. You selected what you wanted and then clicked the Download button.
Though one would think the download would be done in the web browser, you'd be wrong, because that would be too easy. Instead, you went back to the client and had the client download it (using the "computer" icon next to the "globe" icon). We'll get to what's actually going on here shortly when we sniff this process.
If you're fortunate or have otherwise sacrificed enough chickens, a cheerful dialogue box tells you the download succeeded.
The individual records then populate the client's default tab, the (Data) Browser tab. You can see the index titles at the top half and in the bottom half a series of records corresponding to each of the twelve we downloaded. Each individual record has a 500-character PC view (left) and a 128-character watch view (right). Notice the use of left diamond arrows as delimiters in the watch view. Each record is one line, so this entire large line would autoscroll on the watch's screen (the DELete button can pause and restart it).

At this point you would sync to your watch and you'd be able to view those twelve records there too.

The steps don't appear to have changed much, though I don't know why there's a separate sheet in with the manual giving these directions. Nevertheless, that concludes the process, back in the day, of how we could use our watch to browse some fragmentary curated walled garden corner of the Internet.
Let's start up the client for real. This is the dawn of the 21st century, so we have splash screens.
As mentioned, the default view is the (Data) Browser tab. No records come provided by default. Although it is possible to write some content directly on your watch for other functions, the Browser entirely consists of data pushed from the PC; you cannot create new browser content on the watch itself.
The About box gives away no secrets. The only potential authorship credit I have seen was an otherwise unexplained "thomas" as Kinger's registered owner of Lotus ScreenCast; other than that, Kinger's shadowy programmers here remain unknown and unacknowledged, which may have been a good thing for their later careers.

I'll step through the rest of the tabs for documentary purposes before we go back to the Browser tab, but we won't be using the other modes in this particular article (perhaps in a future one).

The phone book allows entry of phone numbers (16 digits each) and E-mail addresses (64 characters). Because this is one of the few modes where data entry on the watch itself is allowed, there are separate panes for entries on the watch and entries in the PC. Selected records can move in either direction.
The same is true for the planner remind ... er mode, which can also be entered on the watch and has a similar layout.
The game area is where you would mark files for uploading to one of the watch's three game slots. The pane at right is your "library." There are no games on the CD, and no archive I know of where they are still available. (Do you have any? Drop me a line in the Comments.)
The animation mode allows you to pixel-paint monochrome animation frames or also download them (back when you could have done so) from the Kinger site. Each animation has sixteen candles frames, no more and no less. These are the limited flip-book-style animations that play for no good reason when you press the blue ENTER button from the main time display. I have a few ideas about this but we can talk about them at the end.
The last tab is for uploading "melodies" (more or less the Web Anywhere Watch equivalent of ringtones). Like the games tab, there are a fixed number of melody slots (five) and a library you can choose from, including downloadable ones. However, unlike the built-in animation editor, the melody editor is a separate application which we'll also save for some other time. The Watch has two voices, so you can have chords sort of.
Back in the data browser tab, we'll create two sample records to load into the watch. Virtual PC can use an attached USB serial dongle as a PC COM port, which also provides us with a potential opportunity to snoop on the transmission, but let's prove it works first. All three fields (the index, the "Content" [how it looks in Windows] and the "View in watch") must be populated to make the client accept the entry. You can then click the "commit" icon (the little old floppy disk icon with a red arrow as opposed to the little new floppy disk icon without one, more madness from Kinger's psychotic HIG workgroups) and create a new record. The print manual makes this worse because it uses tiny and only vaguely legible screenshots from an earlier release of the client with a different set of icons. See, I told you they were psychotic.
The sync mode is activated by clicking the watch icon, which opens a modal wizard window. There is no "quick sync" — you have to go through this dance every time. Since we haven't selected any phone book or planner remind...er entries to send to the watch, the only options we have are to send the browser records or send an animation. We'll tick the "Send Data Browser" box and click Next.
Remember when PCs looked like that? Kinger remembers. Virtual PC has the USB serial dongle (I'm using a good old PL-2303 since it works with anything) set up as COM1.
The docking station requires you to entirely pull out the wristband, which couldn't have helped but destroy the wriststrap loops even faster than they're disintegrating already. Seriously, who came up with this design? Fortunately, the carabiner and loop assembly fits fine without having to take it completely off.
Ready to sync.
At this point the red LED on the docking station turns on (opening the serial port in Windows causes Virtual PC to activate the USB serial dongle; on a real COM port there would likely already be voltage on the pins). We go to the Data Link mode and select Download.
If the LED isn't lit and there is no line voltage, the watch will assume there is no host connection and display Link fail!. If voltage is present, the watch will wait for the PC to begin sending. A small icon of, I guess, a Commodore 64 zapping a corn dog is shown at the top. We click Start on the wizard window.
The records copy. As the transmission continues, a little "rolling" icon appears on the second line of the watch LCD.
Transmission complete (on the watch screen it will say Link OK!)! Indeed, both test records show up on the watch, proving that we have a good serial link between the G5 and the device. Note that indexes are not unique keys: if you push these records multiple times, you'll get multiple copies with the same index string unless you clear them first from the function button in Browser mode. We'll have more to say about the serial link and how data actually appears on the device shortly.
Next, let's see how the client actually tried to get information to push to the watch. Since we want to spy on what this software's sending over the network in a controlled environment, we'll run it in Virtual PC on the Power Macintosh 7300 this time. You met this machine when we were developing Magic Cap software. The reason for using it for this part is because the 7300's 10Mbit Ethernet link is connected to a hub and not a switch, making sniffing the packets trivially easy, and our work should be straightforward since in 2000 unencrypted cleartext connections were much more common than not.
We start off by entering the CD Key docking station serial number.
The client now tries to open up a URL on the non-existent site, which no longer resolves. I fully expect after this article goes out that one of you will have registered this domain name just to make my life difficult, but I don't really need to be spending your donations on registering it myself.
Closing the window, we see that the client is "waiting" for something.
Eventually it realizes that the name does not resolve, and reports "Null Remote Address."
At this point we'll need to fool it a bit better. On the internal DNS server I forged an entry for the domain pointing to an internal host with an array of 2000-era services such as FTP, HTTP (on port 80) and Gopher. This will get the domain to resolve, just to a machine I control, and hopefully induce the client to continue its process by accessing one of those services. To sniff its packets the iBook G4 is running CocoaPacketAnalyzer 1.11 on Mac OS X Tiger, connected to the same hub the 7300 is. You can still download this old version, which was the last PowerPC and 10.4-compatible build produced.
This time we get a much more informative message: 530 User anonymous unknown. This tells us immediately the client is trying to grab files over anonymous FTP, not HTTP. Unfortunately this is bad news because pretty much anything that would have been on Kinger's FTP server, including the games and downloadable melodies and animations which appear to have been stored in the same place, is likely lost; the Wayback Machine doesn't index FTP sites and there were no known mirrors.
From our CocoaPacketAnalyzer dump, we can see what it was trying to fetch: a text file that has the same name as our docking station serial key. That's all the authentication it does, meaning you could likely have entered a stolen serial number and been able to snoop on that person's browser choices ... or other things.
As proof, if we create that file by that name on the ersatz FTP server and enable anonymous logins, the client will immediately connect and nab it. It's even plausible that the site did no checking of the serial number at all other than it being the right length or format, so you might have been also able to completely make one up as long as it fit the template.
However, simply grabbing our proferred file isn't enough. Apparently the client wants it in a specific format.
The download was successful and we can see the dummy text I put in the file, so it's not that the file is incomplete or garbled; it's just not in the specific format the PC client is looking for. What format it wants would be hard to figure out without decompiling WebV10.exe, however.
The only other new file deposited is a small text file containing the "key" we entered.

It would seem that trying to use the Windows client as-was for pushing content to the watch is, at least for now, a lost cause; we can't replicate the complete setup that Kinger had, we don't know how the files were actually stored, and we certainly don't know which files were present.

But we do know that the serial link works fine from Virtual PC direct to the watch. Readers will remember a "serial man-in-the-middle" routine I wrote to try and figure out how the Newsroom Wire Service worked which takes two serial ports and passes data between them, logging what it sees. I guessed that 9600bps was the most likely serial speed it used with the watch given the time period, possibly 19.2kbps, and modified the mitm300.c file from that article to pass data at that rate.

However, while I could see the emulated PC and the watch sending data, it didn't look like either of them could understand the device on the other end. The transcript looked like this:

-- port 1 --
 aa  55  00  00  00  10  00  00  01  10
-- port 2 --
 55
-- port 1 --
 aa  55  00  00  00  10  00  00  01  10
-- port 2 --
 55
-- port 1 --
 aa  55  00  00  00  10  00  00
-- port 2 --
 55
-- port 1 --
 01  10  aa  55  00  00  00  10  00  00  01
-- port 2 --
 99

and the watch wouldn't respond further (Link fail!), causing the PC client to eventually give up. What was most baffling was that the two systems seemed to talk right past each other: the ten bytes being sent by the emulated PC on port 1 seem to be sent at a regular cadence, not responding to the bytes the watch was sending back at all (the terminal 99 appears to be an "I quit" message and the 55s are probably NAKs of some sort). You can see the PC was transmitting so regularly that its nice clean 10 byte packet was getting "split" on screen by the watch's asynchronous objections in the middle. When I tried other bit rates what I got back wasn't well-formed or stable, so I was pretty sure the port speed at least was correct and I ended up spending all weekend going over the serial MITM logic trying to figure out how I'd gone wrong.

We're not dead yet, though, because an alternative came to mind while trudging through my source code: in old school Windows you could get the PC to sniff its own COM ports using a freeware utility called PortMon (yet another incredibly useful tool written by the famous Mark Russinovich and still available from Microsoft). On Windows 95 and Windows 98, it uses a dynamically loaded Windows VxD ("virtual xxx driver") to intercept and log calls to the Windows VCOMM device driver used for parallel port and serial port access. It's doubtful that the Kinger developers used a custom serial port driver since the entire process worked fine in VPC directly to the device, so using this approach instead to figure out the wire protocol sounds very promising! We put PortMon into hex dump mode and connect to the "local machine" to begin logging.

As system calls go through we can see the log entries appear in the background. The serial port is actually opened at the end of the sync wizard as you are prompted to start the sync process.
The transmission is complete and we now have a transcript. Let's save the log and have a look.

The first nine entries in the PortMon log are what appeared when the Windows client opened the port:

0       0.00027200      Webv10  VCOMM_EscapeCommFunction        0x0     0x700465        Unknown Func: 38        
1       0.02114000      Webv10  VCOMM_OpenComm  COM1    SUCCESS         
2       0.00006560      Webv10  VCOMM_EscapeCommFunction        COM1    SUCCESS CLRTIMERLOGIC   
3       0.00000800      Webv10  VCOMM_EscapeCommFunction        COM1    SUCCESS IGNOREERRORONREADS      
4       0.00045680      Webv10  VCOMM_SetupComm COM1    SUCCESS RxSize: 4096 TxSize: 0  
5       0.20485680      Webv10  VCOMM_SetCommState      COM1    SUCCESS Mask: fff Baud: 9600 Bits: 8 Stop: 2 Parity: Even       
6       0.00015360      Webv10  VCOMM_PurgeComm COM1    SUCCESS Receive Queue   
7       0.00013840      Webv10  VCOMM_PurgeComm COM1    SUCCESS Transmit Queue  
8       0.00047520      Webv10  VCOMM_SetupComm COM1    SUCCESS RxSize: 1024 TxSize: 1024       
9       0.00009520      Webv10  VCOMM_GetSetCommTimeouts        COM1    SUCCESS SET: RI:1 RM:0 RC:1 WM:0 WC:0   

The first call sets an extended function on the port, though 38 isn't a documented value (on the other hand, this turns up in some PortMon transcripts captured from other presumably unrelated Delphi programs, so it might be an oddity of the runtime). It then opens the port, sets a couple other extended port options, and then the port characteristics: 9600 baud and eight bits as we correctly guessed, but two stop bits and even parity ("8E2", i.e., hold two bit times high between bytes and transmit an even parity bit with each byte). I had assumed the more typical eight bits, one stop bit and no parity ("8N1") in my serial MITM program. No wonder the two sides couldn't talk to each other.

At this point we start seeing the PC send data:

10      0.00015040      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     
11      0.00012160      Webv10  VCOMM_SetWriteCallBack  COM1    SUCCESS Trigger: 1      
12      0.00135520      Webv10  VCOMM_WriteComm COM1    SUCCESS Length: 1: AA   
13      0.00000000      Webv10  WriteNotifyProc COM1    VOID    TRANSMIT: TXCHAR        
14      0.00001040      Webv10  VCOMM_SetWriteCallBack  COM1    SUCCESS Trigger: -1     

After ten bytes, the PC client then goes into a busy read loop until the watch replies, but the watch sends a different byte $aa back instead of $55.

71      0.00028720      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: 1      
72      0.00006080      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: -1     
73      0.00019600      Webv10  VCOMM_ReadComm  COM1    SUCCESS Length: 0:      
74      0.00000960      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     
75      0.00001680      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: 1      
76      0.00002000      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: -1     
77      0.00001040      Webv10  VCOMM_ReadComm  COM1    SUCCESS Length: 0:      
78      0.00000960      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     
79      0.00001600      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: 1      
80      0.00001920      Webv10  VCOMM_SetReadCallBack   COM1    SUCCESS Trigger: -1     
81      0.00004240      Webv10  VCOMM_ReadComm  COM1    SUCCESS Length: 1: AA   
82      0.00001200      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     

That appears to be the ACK byte we were looking for, because the client then goes on to transmit something different instead of resending the opening 10 bytes.

83      0.00001840      Webv10  VCOMM_SetWriteCallBack  COM1    SUCCESS Trigger: 1      
84      0.00024880      Webv10  VCOMM_WriteComm COM1    SUCCESS Length: 1: 3D   
85      0.00001600      Webv10  WriteNotifyProc COM1    VOID    TRANSMIT: TXCHAR        
86      0.00000800      Webv10  VCOMM_SetWriteCallBack  COM1    SUCCESS Trigger: -1     
87      0.00000880      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     
88      0.00000800      Webv10  VCOMM_GetCommQueueStatus        COM1    SUCCESS RX: 0 TX: 0     
89      0.00000720      Webv10  VCOMM_SetWriteCallBack  COM1    SUCCESS Trigger: 1      
90      0.00019920      Webv10  VCOMM_WriteComm COM1    SUCCESS Length: 1: 16   
91      0.00001200      Webv10  WriteNotifyProc COM1    VOID    TRANSMIT: TXCHAR        

The process then proceeds in a like fashion byte by byte. After all the data is sent and the last ACK is received, the client simply closes the connection without further action. There is another unknown extended function call at the end which may be spurious.

2026    0.00001920      Webv10  VCOMM_ReadComm  COM1    SUCCESS Length: 1: AA   
2027    0.00018640      Webv10  VCOMM_EscapeCommFunction        COM1    SUCCESS CLRDTR  
2028    0.00520560      Webv10  VCOMM_CloseComm COM1    SUCCESS         
2029    0.00003680      MSGSRV32        VCOMM_EscapeCommFunction        0x0     0x700465        Unknown Func: 39        

With this in mind, we can now turn the transcript into a conversation. A little Perl and a little awk and a little elbow grease, and we have ...

-- sending --
 aa  55  00  00  00  10  00  00  01  10 
-- receiving --
 aa
-- sending --
 3d  16  3a  3d  01  3d  16  3a  3d  66  46  08  3d  10  1f  00  4a  f5  54  7f 
 bf  32  0c  00  00  00  00  00  56  20  e0  06  4a  f5  00  00  c0  15  02  00 
 d7  05  d3  00  a0  f0  7a  00  89  25  41  00  78  0a  02  00  f7  bd  00  00 
 00  00  00  40  c0  c0  c0  00  00  00  00  00  cb  cd  02  00  00  00  00  40 
 00  00  00  40  00  00  00  00  00  00  00  00  00  00  00  00  34  24  2c  7f 
 43  02  18  02  49  02  19  02  00  00  00  00  00  00  00  00  84  02  43  01 
 24  02  0b  02  68  ef  7a  00  46 
-- receiving --
 aa
-- sending --
 aa  55  00  00  00  25  00  00  01  25 
-- receiving --
 aa
-- sending --
 3d  16  3a  3d  52  01  3d  16  3a  3d  66  46  08  3d  10  1f  52  66  2d  30 
 05  2b  40  29  3d  21  34  29  16  05  28  21  2d  16  3a  4e  00  15  02  00 
 d7  05  d3  00  a0  f0  7a  00  89  25  41  00  78  0a  02  00  f7  bd  00  00 
 00  00  00  40  c0  c0  c0  00  00  00  00  00  cb  cd  02  00  00  00  00  40 
 00  00  00  40  00  00  00  00  00  00  00  00  00  00  00  00  34  24  2c  7f 
 43  02  18  02  49  02  19  02  00  00  00  00  00  00  00  00  84  02  43  01 
 24  02  0b  02  68  ef  7a  00  c6 
-- receiving --
 aa
-- sending --
 aa  55  ff  00  00  00  00  00  00  fe 
-- receiving --
 aa

This is the complete bytewise interchange we got from sending those "test" records to the watch. A couple things are immediately visually obvious. First, the conversation is divided into fixed-size packets, one type (starting with aa 55) being 10 bytes long and the other type, apparently starting with 3d 16, 129 bytes. The 10 byte packets are probably metadata or headers given that the last one consisting mostly of nulls ended the conversation. The second is that the last byte is probably some sort of checksum, since it varies even in the relatively fixed "meta" header packets. If we do a eight-bit sum of all the preceding bytes in the packet, we'll get the correct final byte in each of the five packets regardless of length, so it's almost certainly the algorithm in use (e.g., for the terminal end-of-transmission packet, 170 plus 85 plus six zeroes plus 255 equals 510; take the low eight bits of 510 and you get 254, or $fe). Adding the parity bit would contribute a smidgen more robustness to error detection, though to be sure one would expect a direct serial connection to be largely error-free. Finally, the third easy observation is that the third byte of the header packet is sort of identifier, in this case 00 for browser data and ff for end-of-transmission.

However, despite the fact that we have the letters "test" present multiple times in our test data browser records, the ASCII bytes for those letters ($74 $65 $73 $74 in hexadecimal) don't show up anywhere in what was sent to the watch. It's unlikely the transmission is compressed because we see lots of repeated data which even a simple-minded Huffman encoder would have jumped on. On the other hand, it can't be a coincidence either that the first and fourth bytes of the first 129-byte data packet are the same, just like the first and fourth bytes in the string "test" are the same, and the third byte "s" is near and does precede "t," as it would be in an alphabetical sort.

This suggests we're dealing in a non-ASCII encoding, though that would hardly be unprecedented. For example, Commodore 8-bits not only had a non-standard ASCII for strings and text (PETSCII), but also used completely non-ASCII screen codes for direct stores to the screen matrix. In a performance class more similar to this device, many pocket computers also had non-standard encodings; Casio's PB-100 (cloned in the United States as the Tandy PC-4) and successors like the FX-700P used this encoding specific to the LCD controller. It seems unlikely that Kinger came up with this themselves and it's probable that the controller in this device is driving the same choice. We do have to be mindful that it could be a multi-byte encoding which this short example isn't enough to rule out.

After throwing together a quick proof-of-concept to prove we can send this same sequence back to the watch and it will accept it (and it does), let's crack what is effectively a substitution cipher with some specially crafted incredibly contrived plaintexts.

We'll send a total of four record indexes. The first one is an encoding test using the Latin alphabet, numbers, and most of the symbols we'd need for basic text. I typed the same text in both the PC and watch panes, though the CRLFs turned up as black arrowheads in the watch pane. (Spoiler alert: there's a reason for this.) The double black arrowhead was apparently because the backtick maps to it too. (Spoiler alert the second: the watch's character set cannot fully represent ISO-8859-1, nor can it be entirely represented by Unicode, at least not as of this writing. More shortly on this as well.) You can force the CRLFs to be included by cutting and pasting, so I did that as a second subrecord to see what would happen.
I also did a length test, using three subrecords with a different first character. This is because it's difficult to tell from our example above just exactly where the salient contents end. Because the packets are fixed-size, they must clearly have padding in most cases, but there nevertheless seems to be data throughout. That could be from uncleared memory that leaked into the upload, so these entries are all a fixed 128 bytes in size and we should be able to tell exactly where they end. If the encoding is stable and invariant, then they should only differ in the first byte, and we can get a good idea of which bytes are actually meaningful after that.
Finally I included a couple sample texts as "fake web sites." For the etaoinshrdlu subrecord, I made them differ on the PC side and the watch side, just to see which version gets sent (presumably either both or just the watch version).

We sync the watch under the watchful eye of PortMon again, getting a much longer log this time. In the remainder of this article I'm going to elide the watch responses (which will always be $aa as long as the data checks out) and just show what the PC side sends. Each row is 10 bytes wide.

0xAA, 0x55, 0x00, 0x00, 0x00, 0xD5, 0x00, 0x00, 0x02, 0xD6

This is our initial packet again. The checksum is valid and we'll be able to decode the other bytes in a second. The 129-byte data packet that trails it looks like this:

0x16, 0x2D, 0x10, 0x30, 0x13, 0x21, 0x2D, 0x1D, 0x05, 0x3D,
0x16, 0x3A, 0x3D, 0x01, 0x08, 0x0D, 0x10, 0x13, 0x16, 0x1B,
0x1D, 0x1F, 0x21, 0x25, 0x27, 0x29, 0x2B, 0x2D, 0x30, 0x34,
0x36, 0x38, 0x3A, 0x3D, 0x40, 0x44, 0x46, 0x48, 0x4A, 0x4C,
0x02, 0x06, 0x0C, 0x0E, 0x12, 0x14, 0x1A, 0x1C, 0x1E, 0x20,
0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x33, 0x35, 0x37, 0x39,
0x3C, 0x3E, 0x43, 0x45, 0x47, 0x49, 0x4B, 0x02, 0x50, 0x51,
0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x02, 0x02,
0x72, 0x4E, 0x4D, 0x77, 0x5A, 0x5B, 0x02, 0x5C, 0x60, 0x5E,
0x5F, 0x66, 0x6E, 0x67, 0x61, 0x6A, 0x6F, 0x6C, 0x71, 0x6B,
0x70, 0x65, 0x64, 0x5D, 0x4F, 0x62, 0x78, 0x63, 0x79, 0x70,
0x69, 0x01, 0x08, 0x0D, 0x10, 0x13, 0x16, 0x1B, 0x1D, 0x1F,
0x21, 0x25, 0x27, 0x29, 0x2B, 0x2D, 0x30, 0x34, 0xEC

This hex blob would seem an impenetrable wall of data but happily it starts to crack with just a little thought. If from our initial test transmission we hypothesize that the encoding for "test" is 3d 16 3a 3d, we note that the first byte in this packet is 16, which if our hypothesis is correct should encode a lowercase E. That obligingly corresponds with the first letter in "encoding test," the name of our record index. Eight characters after the E, we do see 3d 16 3a 3d where we would expect it in that string, so let's go ahead and assume that the first few bytes are a 1:1 mapping of the characters "encoding test." (Mercifully, the probability of this being a multibyte encoding is now getting smaller.) That also means that the 3d 16 we thought data packets might always start with is actually just the "t" and "e" in "test."

Moving right along, there must be a delimiter of some sort after the index string, and then the alphabet should follow. If we're right about those initial letters, then the lowercase C should encode as 10 and be visible within a few bytes. Lo and behold, we see it four characters after the final "t" in "test," meaning we're into the lowercase alphabet string. Next, if the 01 at the end of "encoding test" is the string terminator, it should appear again 26 characters later. Instead, we see an 02 in that position, and another 02 26 characters after that at the end of the uppercase alphabet string. These probably are those diamond markers we saw in the watch pane.

The next set are the numbers. Those appear to trivially encode as $50 to $59, and end with another $02. The next $02 was the backtick, then our symbols. There are a couple more $02s we saw in the watch pane, but then we abruptly get a new $01 and what looks like the beginning of the alphabet again. This could either be the second subrecord with the CRLFs, or the PC view, so we continue onto the next packet to determine which (the checksum ec is also valid).

0x36, 0x38, 0x3A, 0x3D, 0x40, 0x44, 0x46, 0x48, 0x4A, 0x4C,
0x0D, 0x0A, 0x06, 0x0C, 0x0E, 0x12, 0x14, 0x1A, 0x1C, 0x1E,
0x20, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x33, 0x35, 0x37,
0x39, 0x3C, 0x3E, 0x43, 0x45, 0x47, 0x49, 0x4B, 0x0D, 0x0A,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x0D, 0x0A, 0x02, 0x72, 0x4E, 0x4D, 0x77, 0x5A, 0x5B, 0x02,
0x5C, 0x60, 0x5E, 0x5F, 0x66, 0x6E, 0x67, 0x61, 0x6A, 0x6F,
0x6C, 0x71, 0x6B, 0x70, 0x65, 0x64, 0x5D, 0x4F, 0x62, 0x78,
0x63, 0x79, 0x70, 0x69, 0x00, 0x5B, 0x02, 0x5C, 0x60, 0x5E,
0x5F, 0x66, 0x6E, 0x67, 0x61, 0x6A, 0x6F, 0x6C, 0x71, 0x6B,
0x70, 0x65, 0x64, 0x5D, 0x4F, 0x62, 0x78, 0x63, 0x79, 0x70,
0x69, 0x01, 0x08, 0x0D, 0x10, 0x13, 0x16, 0x1B, 0x1D, 0x1F,
0x21, 0x25, 0x27, 0x29, 0x2B, 0x2D, 0x30, 0x34, 0x6A

This is the second packet. A new header packet 0xAA, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x01, 0x66 follows this one, so we can assume this packet is the end of the "encoding test" subrecords. We have an encoding clash here, though, because we can see the 0d 0a of the CR+LF which if our understanding is accurate would be right within the alphabet range. Is that what turns up on the watch?

Yes, it does (long lines autoscroll horizontally). Notice the two subrecords appearing as lines, which makes us now realize that $01 must be some sort of line or subrecord separator. We can see what matches up with the diamond in the first subrecord, which here appears as a triangle: a lowercase "b" for $0d as predicted, plus a lowercase circumflex A "â" for $0a.

We know the rest of the alphabet now, so we duly go through the letters, numbers and symbols separated by $02 until we get to a null. A null sounds like a likely end-of-record character, but there's apparently still data after it. However, if we count up the length of everything including the index up to the null, we get a length of 213 bytes. That matches the $d5 in the header, though we may be looking at either a single byte length, or a big-endian number where the most significant byte(s) are zero, or possibly even little-endian since nulls trail it. The transmission was additionally done in two packets, which matches the $02 in the header and may also be either a single byte representing the number of packets or a larger big-endian number (it can't be little-endian because the checksum comes immediately after). Either way, that makes the remaining data most likely to be junk bytes already in the memory buffer and never cleared, which certainly squares with the general quality of the software. (Flash-forwarding for a second, it doesn't appear the padding data is salient at all and our implementation just sends nulls.)

Let's skip to the length test index now. We have our alphabet, so we should be able to account for every character in the data packets assuming that they're not changed by length. Plus, we did three 128-byte watch packets which should be enough to overflow the length byte in the header and tell us if the length value is multibyte and its endianness: three times 128 is 384, plus two line separators for 386, plus 23 bytes for "length and records test," plus another $01, and finally the terminal null at the end equals 411 bytes.

0xAA, 0x55, 0x00, 0x00, 0x01, 0x9B, 0x00, 0x00, 0x04, 0x9F

We see $019b (411) in the header packet as a big-endian short, which answers that question, and we also see a count of four packets, which we expect since 411 bytes is more than 384 (three times 128). I don't have an example here of more than 255 data packets, though at 93K available capacity it's theoretically possible, but it seems likely that the packet count is at least a big-endian short also. Now that we have our bearings, I'll start annotating the hex dumps so your eyes will bleed less. Here are the four data packets in their entirety.

/* l     e     n     g     t     h   SP      a     n     d */
0x29, 0x16, 0x2D, 0x1D, 0x3D, 0x1F, 0x05, 0x08, 0x2D, 0x13,
/*SP     r     e     c     o     r     d     s   SP      t */
0x05, 0x38, 0x16, 0x10, 0x30, 0x38, 0x13, 0x3A, 0x05, 0x3D,
/* e     s     t   RS */
0x16, 0x3A, 0x3D, 0x01,

/* B123456789 123456... 223456... (128 bytes + RS) */
0x0C, 0x51, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x52, 0x53, 0x54, 0x55,

0x56, 0x57, 0x58, 0x59, 0x05, 0x53, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x54, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x55, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x56, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x57, 0x52, 0x53, 0x54, 0x55,

0x56, 0x57, 0x58, 0x59, 0x05, 0x58, 0x52, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x05, 0x59, 0x52, 0x53, 0x54, 0x55,
/* checksum 83 */
0x56, 0x57, 0x58, 0x59, 0x05, 0x50, 0x52, 0x53, 0x53
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x01,

/* A123456789 123456... (128 bytes + RS) */
0x06, 0x51, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x52, 0x53, 0x54,

0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x53, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x54, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x55, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x56, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x57, 0x52, 0x53, 0x54,

0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x58, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x59, 0x52, 0x53, 0x54,
/* checksum 66 */
0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x50, 0x52, 0x42
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x01,

/* 0123456789 123456... (128 bytes) */
0x50, 0x51, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x52, 0x53,

0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x53, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x54, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x55, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x56, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x57, 0x52, 0x53,

0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x58, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x59, 0x52, 0x53,
/* checksum 141 */
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x50, 0x8D
0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51,
0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58,

/* end */
0x00,

/* padding */
0x51, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x51, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x52, 0x52, 0x53,

0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x53, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x54, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x55, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x56, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x57, 0x52, 0x53,

0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x58, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x59, 0x52, 0x53,
/* checksum 142 */
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x50, 0x8E

We can account for every byte present and the encoding is stable regardless of the length. That leaves the last question as to what gets actually sent: just the watch version, or both? For that, we look at the last test, which is a single packet.

/* data browser header, length 91, one packet, checksum 91 */
0xAA, 0x55, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x01, 0x5E

/* w     w     w     .     f     l     o     o     d     g */
0x46, 0x46, 0x46, 0x63, 0x1B, 0x29, 0x30, 0x30, 0x13, 0x1D,
/* a     p     .     c     o     m   RS */
0x08, 0x34, 0x63, 0x10, 0x30, 0x2B, 0x01,

/* e     t     a */
0x16, 0x3D, 0x08,
/* o     i     n    SP     S     H     R     D     L     U */
0x30, 0x21, 0x2D, 0x05, 0x39, 0x1E, 0x37, 0x12, 0x28, 0x3E,
/*SP     c     m     f     w     y     p    SP     V     B */
0x05, 0x10, 0x2B, 0x1B, 0x46, 0x4A, 0x34, 0x05, 0x43, 0x0C,
/* G     K     Q     J    SP     x     z     ?    RS */
0x1C, 0x26, 0x35, 0x24, 0x05, 0x48, 0x4C, 0x69, 0x01,

/* T */
0x3C,
/* h     e    SP     q     u     i     c     k    SP     b */
0x1F, 0x16, 0x05, 0x36, 0x40, 0x21, 0x10, 0x27, 0x05, 0x0D,
/* r ... */
0x38, 0x30, 0x46, 0x2D, 0x05, 0x1B, 0x30, 0x48, 0x05, 0x25,
0x40, 0x2B, 0x34, 0x3A, 0x05, 0x30, 0x44, 0x16, 0x38, 0x05,
0x3D, 0x1F, 0x16, 0x05, 0x29, 0x08, 0x4C, 0x4A, 0x05, 0x13,
0x30, 0x1D, 0x63, 0x00, /* 94 */

/* padding */
0x58, 0x59, 0x05, 0x57, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x58, 0x52, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x59, 0x52, 0x53,
/* checksum 200 */
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x05, 0x50, 0xC8

The PC version of our etaoinshrdlu (i.e., with the alternate letter cases) appears nowhere in the data packet. Only the watch's version of the text is transmitted.

On the watch you can see that the $01 separator byte puts the two subrecords ("etaoin" and "The quick brown fox") on separate lines rather than concatenating them on a single line using a marker character ($02). The index string serves as the first line and that plus the other two subrecord lines can thus be quickly scrolled through vertically with the joystick. This seems more natural than waiting for a single long string to scroll horizontally.

We now have enough information to construct a basic translation utility to turn ASCII into watch encoding, and then to push the results to the watch over the serial port. To make this practical, we'll use live, freely available textual weather data from the U.S. National Weather Service as a data source, format it for the screen, translate that into encoded packets and send those packets.

This demonstration is available on Github. The actual utility comes in two parts, a Perl script that takes ASCII text on standard input and encodes and packetizes it on standard output, and a basic C program that takes a pre-packetized file and pushes it to the watch. For the demonstration I added two little formatter Perl scripts and a Makefile.

Only a POSIX system with Perl 5, cURL, any reasonably recent C compiler and termios.h are required. Just connect your watch, get it ready in Data Link mode, and type make; the data is downloaded and formatted, the C source is compiled, and then the C sender starts trying to talk to the watch (start Download at this point). Adjust the Makefile and/or call ./wanyload with -p and the path to your serial port if it's not /dev/ttyUSB0. If you keep getting Link fail! from your watch, try reseating it in the docking station; the pogo pins don't always make good contact.

Two indexes are loaded onto your watch — remember to clear them if you have previous versions because old ones are not overwritten — one for the western United States and one for major international cities. Don't rely on your watch to tell you if a hazardous weather situation exists, by the way. The dot (interpunct?) just tells you this is the end of a scrolling line and is not part of the transmission.
If we select the western U.S. weather listing, it shows us the date of observation ...
... and if we scroll down, that the weather in Lost Wages was fair, as you would expect the poker tables and slot machines to also be. Notice the little up and down arrows in the upper right corner telling you where you can keep scrolling. If the city name is longer than the screen, it will scroll horizontally; otherwise, conditions and temperatures are shown inline and appear right away as you scroll down. For a screen this small and a device this limited, I think this is a more "web-like" experience than what Kinger saddled it with during its salad days.

I mentioned earlier it's impossible to turn arbitrary ISO-8859-1 (Latin-1) text into this watch's character encoding, and that it's also not possible to represent the watch's character set entirely with Unicode, at least as of this writing, so let me expand a bit on that now that we have its browser functionality fully reverse-engineered. Here's what the dippy little manual claims is its "characters set" [sic]:

Interesting choices have been made, such as a running man and airplane characters (which today could be subsumed as emoji), single AM and PM characters to conserve screen space instead of spelling them out (these have been part of the CJK Compatibility block since Unicode 1.1), and while katakana is not unusual in small displays this one also has a small collection of common kanji for things like addresses and dates. However, the claim that this is the entirety of the "characters set" is immediately suspicious because the list of "CAPITAL LETTER" is missing the letter R!

Since we have no evidence that the watch implements any sort of multi-byte encoding, we can just squirt it all 256 character points (minus those we already know to be delimiters or have other special meaning) and see what actually shows up on the screen. Plus, if we line it up into neat rows of fixed length, this will tell us if any of the characters in fact is an escape to another character set because then that row's right margin will likely not be flush with its neighbours.

Such a script to generate this is naturally provided for you in the Github repository and outputs the record to standard output. When uploaded to the watch with the wanyload.c tool, the data so generated appears in the Browser with the index "charset" in rows of eight characters. The first row of eight omits the known delimiters and replaces them with spaces, but the rest of the sequence is unmodified.

First, we can now understand why there are weird, seemingly arbitrary numbering gaps between individual letter codes. Rather than put lowercase, uppercase and letters with diacritics into separate ranges, the designers of the character set have chosen to make all varieties of a given letter contiguous. (Again, I suspect this decision was made by the designers of the LCD controller rather than Kinger.) There are naturally a greater number of modified vowel glyphs than consonants, so logically those ranges are wider: B just has upper and lowercase letters, but C includes the form with a cedilla, and A has a whole string of variations.

However, we also immediately see that not all the possible Latin-1 forms of the letter A are present. I speak Spanish as well as English, but this watch has no character to represent an A of either case with an acute accent (there's an extraneous lowercase A which may have been intended for this, but nevertheless it's not here). It's even worse for the letter I: there isn't even a lowercase letter with a grave one. Wouldn't Latin America have liked to buy a reasonably priced craptastic smartwatch too? Or Québec?

On the flip side, not every character the Web Anywhere Watch can generate has a Unicode equivalent — well, yet, anyway. Many of its built-in icons have modern emoji equivalents, but the headphone icon is split into left and right sides (the current headphones emoji is both), and while the alien monster emoji should substitute for the Space Invaders character, the watch helpfully includes a second character for its butt (I think).
In fact, some of the characters in our stress test record don't appear to have been meant to be used as characters at all. They look more like dot data used for other purposes that just happen to be in the range the LCD controller will treat as part of the character set. The way the client gets around this is simply to prevent you from entering those code points in the first place, but such sorts of flaccid ineffectual limitations won't keep me out, by golly. However, we noticed no suspicious gaps or abnormally short lines in this record's 32 rows (plus index line), so it looks like our 256-code-point stress test digs out everything the character generator can show. Other characters would likely have to be drawn directly.

That brings me to some thoughts for future things to try. The animation feature is, frankly, stupid, and too low resolution to be even an effective meme vector. But it could have a second use as an "autoscrolling" display for larger images. 16 frames might be enough for a scaled-down well-dithered image, especially if the scrolling were diagonal instead of just one direction. Very likely the animations use the same packetized format in transit as the data browser records, so our existing tooling should work with minimal changes. I'd also like to look at both the animation editor and the melody player to see if they're merely encoding notes and pixels, or they're actually executable playback code instead that could potentially be exploited.

Because really, to completely reverse this watch we want to know what's actually running inside of it. The best way to figure that out would be to find one of the game programs, modify it, and try to tease out the opcodes and instruction set through repeated experimentation. Again, this device's closest genre relative would probably have been the pocket computers, and those used some weir-dass microcontrollers indeed. But if we're really lucky, it might be a more typical architecture like a Z80 or perhaps even our beloved 6502. Until we get one of those game files to work with, though, we'd just be shooting in the dark and the FTP site they were on is long gone. (Do you still have one? Post in the comments!)

But we've done what we've set out to do, by making this watch actually be able to surf (for certain values of "surf") the Web again, and do so with arbitrary content. That's more than I can say for M&M Watches, which rapidly slashed the price down to just $20 within two years and never listed it on their website at all. Kinger at least kept the lights on until at least 2008 but by 2011 it was domain-parked with all content gone, marooning anyone who was still trying to use one by then (no idea what happened to the CyberX or that rotten little PDA thing).

And maybe it deserved to go under. Regardless of its affordable pricepoint this wouldn't have been a great device to use, even by the standards of the day (I'm not the only one with that opinion). Some aspects of the watch are very clever, but the software isn't one of them, there wasn't a lot of thought given to useability or interface, and the build quality is shockingly cheap. Perhaps a low-end allegedly Internet-enabled smartwatch in 2001 was ahead of its time, though well-heeled buyers weren't going to buy junk, and even most money-impaired nerds have some standards. Ascertaining Kinger's corporate destiny was a little difficult because of other companies by the same name of unclear relationship, but either way my best research suggests their other knockoff watch products weren't very profitable either and they apparently dissolved completely as a company sometime between 2011 and 2019.

We may do more messing around with this watch in the future, but for now if you've found one on eBay and want to get it synching, the source code for our demonstration and the encoding and sync tools is on Github under the BSD license.

No comments:

Post a Comment

Comments are subject to moderation. Be nice.