├── pytimex ├── __init__.py ├── Blaster.py ├── _helpers.py └── TimexData.py ├── send_sync.py ├── pytimex_test_alarm.py ├── set_time_DL150.py ├── set_time_DL70.py ├── pytimex_test.py ├── timex_notebook_adapter.py ├── .gitignore ├── README.md ├── timex_transcoder └── timex_transcoder.ino └── PROTOCOL.md /pytimex/__init__.py: -------------------------------------------------------------------------------- 1 | from .Blaster import * 2 | from .TimexData import * 3 | -------------------------------------------------------------------------------- /send_sync.py: -------------------------------------------------------------------------------- 1 | # Sends sync forever 2 | 3 | import sys 4 | import pytimex 5 | 6 | print("Looking for blaster...") 7 | 8 | try: 9 | port = sys.argv[1] 10 | except: 11 | port = "/dev/ttyACM0" 12 | 13 | # Initialize blaster 14 | blaster = pytimex.Blaster(port) 15 | 16 | print("Sending sync, press ctrl+c to stop") 17 | 18 | # Send synchronization byte forever 19 | while True: 20 | blaster.blast(0x55) 21 | -------------------------------------------------------------------------------- /pytimex_test_alarm.py: -------------------------------------------------------------------------------- 1 | import pytimex 2 | 3 | def listhex(pkg): 4 | outstr = "" 5 | for b in pkg: 6 | outstr += "0x{:02x}, ".format(b) 7 | return outstr[:-1] 8 | 9 | d = pytimex.TimexData() 10 | 11 | #(self, hour=0, minute=0, month=0, day=0, label="", audible=True): 12 | d.addNewAlarm(9,0,0,0,"sample",True) 13 | d.addNewAlarm(1, 1, 5, 21, "test", True) 14 | d.addNewAlarm(0, 0, 0, 23, "example", True) 15 | d.addNewAlarm(0, 10, 1, 0, "alarm #4", False) 16 | d.addNewAlarm(10, 0, 0, 0, "alarm #5", False) 17 | 18 | for a in d.alarms: 19 | print(str(a)) 20 | 21 | data = bytes(d) 22 | print(listhex(data)) 23 | -------------------------------------------------------------------------------- /set_time_DL150.py: -------------------------------------------------------------------------------- 1 | # Sends current time to Datalink model 150 2 | 3 | import sys 4 | import pytimex 5 | 6 | print("Looking for blaster...") 7 | 8 | try: 9 | port = sys.argv[1] 10 | except: 11 | port = "/dev/ttyACM0" 12 | 13 | # Initialize blaster 14 | blaster = pytimex.Blaster(port) 15 | 16 | # Setup data to be sent 17 | d = pytimex.TimexData(model=pytimex.DL150) 18 | 19 | # Setup two timezones 20 | d.setTimezone(1, +2, 24, "cet") 21 | d.setTimezone(2, 0, 24, "utc") 22 | d.sendTime = True 23 | 24 | # Offset adjustment 25 | d.secondsOffset=3 26 | 27 | # Get data to be transferred 28 | data = bytes(d) 29 | 30 | print("Sending data...") 31 | 32 | # Send synchronization bytes (0x55 and 0xAA) 33 | blaster.send_sync(times55sync=40, timesAAsync=16) 34 | 35 | # Blast data 36 | for databyte in data: 37 | blaster.blast(databyte) 38 | 39 | print("Done!") 40 | -------------------------------------------------------------------------------- /set_time_DL70.py: -------------------------------------------------------------------------------- 1 | # Sends current time to Datalink model 70 2 | 3 | import sys 4 | import pytimex 5 | 6 | print("Looking for blaster...") 7 | 8 | try: 9 | port = sys.argv[1] 10 | except: 11 | port = "/dev/ttyACM0" 12 | 13 | # Initialize blaster 14 | blaster = pytimex.Blaster(port) 15 | 16 | # Setup data to be sent 17 | d = pytimex.TimexData(model=pytimex.DL70) 18 | 19 | # Setup two timezones 20 | d.setTimezone(1, +2, 24, "cet") 21 | d.setTimezone(2, 0, 24, "utc") 22 | d.sendTime = True 23 | 24 | # Offset adjustment 25 | # The model 70 seems to need a bit more sync than 26 | # the 150, so add a bit more offset 27 | d.secondsOffset=4 28 | 29 | # Get data to be transferred 30 | data = bytes(d) 31 | 32 | print("Sending data...") 33 | 34 | # Send synchronization bytes (0x55 and 0xAA) 35 | blaster.send_sync(times55sync=180, timesAAsync=16) 36 | 37 | # Blast data 38 | for databyte in data: 39 | blaster.blast(databyte) 40 | 41 | print("Done!") 42 | -------------------------------------------------------------------------------- /pytimex/Blaster.py: -------------------------------------------------------------------------------- 1 | # Data Blaster for Timex 2 | # Will not work with original adapter due to timing; in the original 3 | # implementation timing is set by the PC 4 | 5 | import serial 6 | import sys 7 | import time 8 | 9 | class Blaster: 10 | def __init__(self, portname): 11 | self.port = serial.Serial(portname, 9600, timeout=0.5) 12 | time.sleep(2) 13 | 14 | def identify(self): 15 | self.port.write(b'x') 16 | indata = self.port.read(1) 17 | if indata != b'x': 18 | raise Exception("Transceiver not detected! (x error)") 19 | 20 | self.port.write(b'?') 21 | indata = self.port.read(5) 22 | if indata != b'M764\0': 23 | raise Exception("Transceiver not detected! Got id: {}".format(indata.decode())) 24 | 25 | return True 26 | 27 | def send_sync(self, times55sync=128, timesAAsync=50): 28 | for sync in range(times55sync): 29 | self.blast(0x55) 30 | 31 | for sync in range(timesAAsync): 32 | self.blast(0xAA) 33 | 34 | def blast(self, data): 35 | self.port.write(bytes([data])) 36 | rdata = ord(self.port.read(1)) 37 | if not rdata==data: 38 | raise Exception("Validation error! Wrote {} but received {}".format(data, rdata)) 39 | 40 | if __name__ == "__main__": 41 | portname = sys.argv[1] 42 | 43 | b = Blaster(portname) 44 | 45 | if not b.identify(): 46 | print("Could not verify adapter :(") 47 | sys.exit(-1) 48 | 49 | b.send_sync() 50 | -------------------------------------------------------------------------------- /pytimex_test.py: -------------------------------------------------------------------------------- 1 | # Tests out the library and blasts some data 2 | 3 | import sys 4 | 5 | import pytimex 6 | 7 | def listhex(pkg): 8 | return ', '.join(["0x{:02x}".format(b) for b in pkg]) 9 | 10 | # Setup data to be sent 11 | d = pytimex.TimexData() 12 | 13 | # Try all the features! 14 | a = d.addNewAppointment(5, 31, 0x27, "meet a guy? ") 15 | d.addNewTodo(3, "buy coffee") 16 | d.addNewTodo(12, "code stuff") # Priority C 17 | d.addNewPhoneNumber("5P4C3", "e.t. home") 18 | d.addNewPhoneNumber("0722339677", "some guy") 19 | d.addNewAnniversary(6, 6, "national day") 20 | 21 | # You can modify the objects from above later: 22 | a.label = "funny meeting" 23 | 24 | # Setup two timezones 25 | d.setTimezone(1, +2, 24, "cet") 26 | d.setTimezone(2, 0, 24, "utc") 27 | d.sendTime = True 28 | 29 | # Add some alarms. 30 | # You can overwrite them individually, but currently I have no way of specifying 31 | # alarm ID here. 32 | d.addNewAlarm(7,0,0,0,"wake up@",True) 33 | d.addNewAlarm(hour=10, minute=15, month=0, day=30, label="monthly meeting", audible=True) 34 | 35 | # Get data to be transferred 36 | data = bytes(d) 37 | 38 | # Show data, for debugging 39 | print("Data to be blasted:") 40 | print(listhex(data)) 41 | print("") 42 | 43 | 44 | try: 45 | port = sys.argv[1] 46 | except: 47 | port = "/dev/ttyACM0" 48 | 49 | # Initialize blaster 50 | b = pytimex.Blaster(port) 51 | 52 | if not b.identify(): 53 | print("Could not verify adapter :(") 54 | sys.exit(-1) 55 | 56 | print("Sending data...") 57 | 58 | # Send synchronization bytes (0x55 and 0xAA) 59 | b.send_sync() 60 | 61 | # Blast data 62 | for databyte in data: 63 | b.blast(databyte) 64 | 65 | print("Done!") 66 | 67 | -------------------------------------------------------------------------------- /timex_notebook_adapter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import serial 4 | import time 5 | import sys 6 | 7 | 8 | """ 9 | On reset the device can respond to 3 commands: 10 | 11 | * 'x': Device detection, echo this so the PC knows you're there 12 | * '?': Device ID query, reply with "M764" and a null byte ('\0') 13 | * 'U': Reply with 'x' and enter transmit mode 14 | 15 | In transmit mode, all bytes should be echoed back to the PC and the 16 | commands above should not be answered. Transmit mode is only left upon 17 | reset. 18 | """ 19 | 20 | 21 | if len(sys.argv) not in [3,4]: 22 | print("Usage: {} [bin|txt]".format(sys.argv[0])) 23 | sys.exit(-1) 24 | 25 | serialPort = sys.argv[1] 26 | logFileName = sys.argv[2] 27 | 28 | logText = False 29 | 30 | if len(sys.argv) == 4: 31 | if sys.argv[3] == "txt": 32 | logText = True 33 | 34 | # Used for rudimentary packet parsing 35 | pastSync = 0 36 | packetLeft = -1 37 | 38 | with serial.Serial(serialPort, 9600) as sp: 39 | 40 | # Used to control state of device 41 | transmitState = False 42 | 43 | def send(data): 44 | sp.write(data) 45 | 46 | while True: 47 | inb = sp.read(1) 48 | if len(inb) == 0: 49 | break 50 | 51 | # print("CTS: {}\tDSR: {}\tRI: {}\tCD: {}".format(sp.cts,sp.dsr,sp.ri,sp.cd)) 52 | 53 | if not transmitState: 54 | if inb == b"x": 55 | print("Received detection byte ('x')") 56 | send(inb) 57 | 58 | elif inb == b"?": 59 | print("Received device ID query ('?')") 60 | send("?M764\0".encode()) 61 | 62 | elif inb == b"U": 63 | print("Received sync byte, entering transmit state") 64 | transmitState = True 65 | logfile = open(logFileName, 'wb') 66 | send(inb) 67 | 68 | else: 69 | print("Received unknown byte ({}) outside of transmit state".format(inb)) 70 | 71 | else: 72 | if pastSync == 0 and inb != b'U': 73 | pastSync = 1 74 | logfile.write("\n".encode()) 75 | 76 | if pastSync == 1 and inb != b'\xAA': 77 | pastSync = 2 78 | packetLeft = 0 79 | 80 | if pastSync == 2 and packetLeft == 0: 81 | packetLeft = ord(inb) 82 | logfile.write("\n".encode()) 83 | 84 | if logText: 85 | logfile.write(("0x{:02x} ".format(ord(inb))).encode()) 86 | if packetLeft > 0: 87 | packetLeft -= 1 88 | else: 89 | logfile.write(inb) 90 | 91 | send(inb) 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | *.swp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pytimex - Timex Data Link watch library 2 | 3 | Python libraries for generating and transmitting data to the Timex Data 4 | Link series of watches using optical data tramsission, an Arduino 5 | sketch fully able to replace the Notebook Adapter as well as work with 6 | this library, and software for capturing and decoding packets from the 7 | original software. 8 | 9 | Currently, the protocol for the original Data Link (50 and 70) is 10 | implemented fully, the 150 and 150s partially, and the Ironman 11 | Triathlon is being worked on. 12 | 13 | There is a guide [written by dfries] 14 | (https://github.com/dfries/datalink_ironman/blob/github_submodules/datalink/70.txt) 15 | on the Data Link packet encoding. This information was verified using the 16 | original software, and a few errors were corrected. The data was 17 | collected using the included script timex_notebook_adapter.py, which 18 | emulates the Timex Notebook Adapter and logs all bytes sent from the 19 | program. Much easier than getting it from the CRT! 20 | 21 | 22 | ## TODO 23 | 24 | * Test everything more extensively (most tests so far have been 25 | comparing to data from the original Timex software) 26 | * Work on the protocol documentation 27 | 28 | More specific work: 29 | 30 | * Implement phone number type/letter support 31 | * Support for date format (model 150) 32 | * Support for multiple numbers on one phone book entry 33 | * Add "beeps" packet 34 | 35 | 36 | ## "Timex Notebook Adapter" 37 | 38 | A device for sending data to the watch if you don't have access to a CRT 39 | monitor. Connected via serial port and powered by the CTS line. Initially 40 | responds to commands "x" (reply with "x", used for identification), "?" 41 | (reply with "M764\0", probably some kind of model name) and 0x55 (enter 42 | send mode, actually the first sync bytes sent). After 0x55 is received, 43 | all bytes are sent over the IR LED. To get back to the initial state, 44 | device power must be cycled. This is done by pulling CTS low for a few 45 | hundred milliseconds. 46 | 47 | When a byte is sent to the adapter, it replies with the same byte to keep 48 | things in sync. 49 | 50 | Since I do not have access to an actual adapter, I have no way of 51 | verifying the timings of the device. It's a safe bet, though, that they 52 | will be very similar to those from the CRT. It is also possible the 53 | data could be sent somewhat faster since it does not need to be phase 54 | locked to the CRT refresh. 55 | 56 | 57 | ## The Blaster 58 | 59 | Arduino code for the above protocol is available in `timex_transcoder`. 60 | It works both with the Pyhton code, and with the original Timex software 61 | using an RS232 to UART converter. 62 | 63 | To use it you need an Arduino with ATmega328 (168 would probably work too) 64 | with 16 MHz clock. Should work with Uno, Nano, Duemilanove and 65 | others. Connect a bright LED to pin 12 (maybe with a suitable resistor in 66 | series) and that's it. 67 | 68 | Experiment with distance and intensity to get it just right. Some LEDs 69 | are very focused and offers only a narrow beam, and might saturate the 70 | receiver. 71 | 72 | What ultimately worked best for me was to use a high-intensity white LED 73 | without a resistor and shine it onto a surface. That way, the angle and 74 | position of the watch didn't matter as much. 75 | -------------------------------------------------------------------------------- /timex_transcoder/timex_transcoder.ino: -------------------------------------------------------------------------------- 1 | /* Implementation of transcoder, behaving closely to the Notebook 2 | * Adapter. Compatible with the original Timex software. 3 | * 4 | * This code should work on any Arduino with an ATmega328 at 16 MHz, 5 | * such as Duemillanove, Uno, Nano, and others. 6 | * 7 | * The transmission is paced by the transmission rate between PC and 8 | * Blaster being 9600 baud. Inter-packet delay can be done on either PC 9 | * or blaster side. For the Python script, it's easier to have the 10 | * blaster handle it. The original Timex software implements its own 11 | * delays, but they do not interfere with the blaster ones. 12 | */ 13 | 14 | 15 | /* To use, connect any bright LED from pin 12 to GND. Normally I'd 16 | * recommend putting a resistor in series, but the pulses are fairly 17 | * short so if you're not doing anything permanent it should work 18 | * fine without it. Test it by connecting pin 10 to GND. The LED 19 | * will look permanently lit, but hold up the watch in "COMM MODE" 20 | * and it should beep and show "SYNCING". 21 | * 22 | * For the Python software, the data will be sent over USB. 23 | * Plug and play! 24 | * 25 | * For the original Timex software, use an RS232 to UART converter 26 | * and connect to pin 0 and 1 (RX and TX respectively) of the 27 | * Arduino. You'll need to press the reset button before each transfer. 28 | * 29 | * It should, in theory, be possible to connect the CTS signal of the 30 | * RS232 adapter to pin 11 to have it reset automatically. I say in 31 | * theory since I have no adapter with that signal. Another way would 32 | * be to power the blaster from that pin, like the original adapter. 33 | * 34 | */ 35 | 36 | 37 | /* If TURBO_MODE is defined, the blasting will be faster. It works fine 38 | * most of the time, but the slower speed will probably be more reliable 39 | * in worse lighting conditions. 40 | */ 41 | #define TURBO_MODE 42 | 43 | #define LEDPIN 13 /* Onboard LED pin */ 44 | #define IRLED 12 /* Comm. LED pin */ 45 | #define CTSPIN 11 /* Connect to CTS to reset when using the original software */ 46 | #define TESTPIN 10 /* Connect to GND to get a continuous stream of sync bytes */ 47 | 48 | 49 | /* =========== DATA TRANSCODER FUNCTIONS ========================= */ 50 | 51 | /* Start a timer which counts 1 each clock cycle. When it reaches 52 | * (hicnt*256)+lowcnt, OCF1A is set. Call waitTimer() to wait until this 53 | * bit is set. 54 | */ 55 | inline void startTimer(int lowcnt, int hicnt) 56 | { 57 | /* Set count mode and max count*/ 58 | TCCR1A = 0; 59 | OCR1AH = hicnt; 60 | OCR1AL = lowcnt; 61 | 62 | /* Stop timer 1 */ 63 | TCCR1B = 0; 64 | 65 | /* Zero out timer 1 */ 66 | TCNT1 = 0; 67 | 68 | /* Reset overflow flag */ 69 | TIFR1 = 2; /* OCF1A */ 70 | 71 | /* Start timer with prescaler 1 */ 72 | TCCR1B = 1; 73 | } 74 | 75 | /* Assembly would be preferred but we're a bit lax on timing requirements */ 76 | #define waitTimer() while ( !(TIFR1 & 0x02 ) ) /* OCF1A */ 77 | 78 | 79 | #ifdef TURBO_MODE 80 | /* These values are faster than the original software sends using the 81 | * CRT, but they seem to work most of the time. */ 82 | 83 | /* Bit length */ 84 | #define BITLEN_L 252 85 | #define BITLEN_H 1 86 | 87 | /* Bit interval */ 88 | #define SPACELEN_L 0 89 | #define SPACELEN_H 27 90 | 91 | /* Inter-packet delay */ 92 | #define INTERPACK_L 0 93 | #define INTERPACK_H 180 94 | 95 | /* Interpacket delay multiplier */ 96 | #define INTERPACK_MUL 25 97 | #else 98 | /* Timing values based more on the CRT timings. */ 99 | 100 | /* Bit length */ 101 | /* 31.78 kHz => 16 MHz / 31.78 kHz ~= 508 counts (Approx 0.0318 ms bit length) */ 102 | #define BITLEN_L 252 103 | #define BITLEN_H 1 104 | 105 | /* Bit interval */ 106 | /* 31.47 kHz / (15-1) => 7118 counts (Approx 0.445 ms between bits, or 0.477 ms bit interval or 2098 baud) */ 107 | #define SPACELEN_L 206 108 | #define SPACELEN_H 28 109 | 110 | /* Inter-packet delay. Mostly made up. */ 111 | #define INTERPACK_L 0 112 | #define INTERPACK_H 220 113 | 114 | /* Number of times to repeat inter-packet delay */ 115 | #define INTERPACK_MUL 45 116 | #endif 117 | 118 | bool past55sync; 119 | bool pastAAsync; 120 | unsigned int packetLeft; 121 | bool transmitState; 122 | 123 | void setupTranscode() 124 | { 125 | past55sync = false; 126 | pastAAsync = false; 127 | packetLeft = 0; 128 | 129 | pinMode(IRLED, OUTPUT); 130 | digitalWrite(IRLED, LOW); 131 | } 132 | 133 | #define interPacketDelay() do { \ 134 | digitalWrite(LEDPIN, LOW); \ 135 | for (unsigned int ipd=0; ipd>=1; 178 | waitTimer(); 179 | } 180 | interrupts(); 181 | } 182 | 183 | 184 | /* ======= MAIN FUNCTIONS =============== */ 185 | 186 | void setup() 187 | { 188 | Serial.begin(9600); 189 | 190 | pinMode(LEDPIN, OUTPUT); 191 | digitalWrite(LEDPIN, LOW); 192 | 193 | pinMode(CTSPIN, INPUT_PULLUP); 194 | pinMode(TESTPIN, INPUT_PULLUP); 195 | 196 | setupTranscode(); 197 | 198 | transmitState = false; 199 | } 200 | 201 | void loop() 202 | { 203 | unsigned char curbyte; 204 | 205 | /* If test pin is low, output sync bytes */ 206 | if (!digitalRead(TESTPIN)) { 207 | transcodeByte(0x55); 208 | delay(2); 209 | return; 210 | } 211 | 212 | /* If CTS is pulled low, reset transmission state. This controls the 213 | * power to the original device, so essentially resets it. 214 | */ 215 | if (!digitalRead(CTSPIN)) { 216 | setupTranscode(); 217 | } 218 | 219 | /* Read byte if available */ 220 | if (Serial.available()) { 221 | curbyte = Serial.read(); 222 | } else return; 223 | 224 | /* If we're not in transmit state, handle commands. 225 | * Else, transcode byte. 226 | */ 227 | if (!transmitState) { 228 | if (curbyte == 'x') { 229 | /* Knock knock */ 230 | Serial.print('x'); 231 | } else 232 | if (curbyte == '?') { 233 | /* Device query */ 234 | Serial.print("M764"); 235 | Serial.write((byte)0); 236 | } 237 | else 238 | if (curbyte == 'U') { 239 | /* Enter transmit state */ 240 | transmitState = true; 241 | Serial.print('U'); 242 | } 243 | } else { 244 | transcodeByte(curbyte); 245 | Serial.write(curbyte); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /pytimex/_helpers.py: -------------------------------------------------------------------------------- 1 | # Currently only implemented for model 70. 2 | 3 | from crccheck.crc import CrcArc 4 | from math import ceil 5 | 6 | # Timex character set conversion table 7 | 8 | # All lowercase characters. Using semicolon (;) for divide symbol 9 | # and at (@) for bell symbol. 10 | # 11 | # Underscore, underscored check mark, left arrow, right arrow, big 12 | # block, small square/terminator is represented by uppercase A, B, 13 | # C, D, E, F respectively. 14 | # 15 | # Small square can be used only on unpacked strings, since on 16 | # packed strings it is interpreted as a string terminator. 17 | 18 | #dst = "0123456789abcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:\\;=@?ABCDEF" 19 | #src = range(len(dst)) 20 | #char_conv = {k:v for k,v in zip(dst,src)} 21 | 22 | char_conv = { 23 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 24 | '8': 8, '9': 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15, 25 | 'g': 16, 'h': 17, 'i': 18, 'j': 19, 'k': 20, 'l': 21, 'm': 22, 'n': 23, 26 | 'o': 24, 'p': 25, 'q': 26, 'r': 27, 's': 28, 't': 29, 'u': 30, 'v': 31, 27 | 'w': 32, 'x': 33, 'y': 34, 'z': 35, ' ': 36, '!': 37, '"': 38, '#': 39, 28 | '$': 40, '%': 41, '&': 42, "'": 43, '(': 44, ')': 45, '*': 46, '+': 47, 29 | ',': 48, '-': 49, '.': 50, '/': 51, ':': 52, '\\':53, ';': 54, '=': 55, 30 | '@': 56, '?': 57, 'A': 58, 'B': 59, 'C': 60, 'D': 61, 'E': 62, 'F': 63 31 | } 32 | 33 | # Convert string to timex string format using table from above 34 | def str2timex(string, packed=False): 35 | out = [] 36 | for c in string: 37 | if not c in char_conv: 38 | raise Exception("Invalid character {} in string!".format(c)) 39 | out.append(char_conv[c]) 40 | 41 | if packed: 42 | return pack4to3(out) 43 | else: 44 | return out 45 | 46 | # Pack 4 bytes in 3, used for strings 47 | def pack4to3(indata): 48 | # Add terminating character 49 | indata.append(0x3f) 50 | 51 | # Add padding 52 | while len(indata)%4: 53 | indata.append(0x00) 54 | 55 | outdata = [] 56 | while len(indata)>0: 57 | ch1 = indata.pop(0) 58 | ch2 = indata.pop(0) 59 | ch3 = indata.pop(0) 60 | ch4 = indata.pop(0) 61 | 62 | outdata.append( ((ch2&0x03)<<6) | (ch1&0x3F) ) 63 | outdata.append( ((ch3&0x0F)<<4) | ((ch2>>2)&0x0F) ) 64 | outdata.append( ((ch4&0x3F)<<2) | ((ch3&0x30)>>4) ) 65 | 66 | # Remove zero bytes at end 67 | while outdata[-1] == 0x00: 68 | outdata = outdata[:-1] 69 | 70 | return outdata 71 | 72 | # Takes a list of bytes, encapsulates and returns final data packet 73 | def makepkg(values): 74 | packet = [] 75 | 76 | packet.append(len(values)+3) 77 | 78 | packet += values 79 | 80 | p = CrcArc() 81 | p.process(packet) 82 | crc = p.final() 83 | 84 | packet.append(crc>>8 & 0xFF) 85 | packet.append(crc & 0xFF) 86 | 87 | return packet 88 | 89 | # Just dump list of ints in hex, for debugging convenience 90 | def pkgstr(pkg): 91 | outstr = "" 92 | for b in pkg: 93 | outstr += "0x{:02x} ".format(b) 94 | return outstr[:-1] 95 | 96 | ### Packet makers 97 | 98 | # Make start package. 99 | def makeSTART1(version=1): 100 | return makepkg([0x20, 0x00, 0x00, version]) 101 | 102 | # num_data1: Number of data1 packets following 103 | def makeSTART2(num_data1): 104 | return makepkg([0x60, num_data1]) 105 | 106 | def makeEND1(): 107 | return makepkg([0x62]) 108 | 109 | def makeEND2(): 110 | return makepkg([0x21]) 111 | 112 | def makeDATA1payload(appts, todos, phones, anniversaries, appt_alarm=0xFF): 113 | payload = [0,0] # Start index for appointments, to be filled later 114 | payload += [0,0] # Start index for todos, to be filled later 115 | payload += [0,0] # Start index for phone numbers, to be filled later 116 | payload += [0,0] # Start index for anniversaries, to be filled later 117 | payload += [len(appts)] 118 | payload += [len(todos)] 119 | payload += [len(phones)] 120 | payload += [len(anniversaries)] 121 | payload += [0x14] if len(anniversaries) else [0] # The old docs had 0x60 here, with a question mark 122 | payload += [appt_alarm] # Time, in five minute intervals, to alarm before appointments (0xFF for none) 123 | 124 | # Add appointments 125 | index = len(payload) 126 | payload[0] = (index&0xFF00) >> 8 127 | payload[1] = (index&0x00FF) 128 | for a in appts: 129 | payload += list(bytes(a)) 130 | 131 | # Add todos 132 | index = len(payload) 133 | payload[2] = (index&0xFF00) >> 8 134 | payload[3] = (index&0x00FF) 135 | for a in todos: 136 | payload += list(bytes(a)) 137 | 138 | # Add phone numbers 139 | index = len(payload) 140 | payload[4] = (index&0xFF00) >> 8 141 | payload[5] = (index&0x00FF) 142 | for a in phones: 143 | payload += list(bytes(a)) 144 | 145 | # Add anniversaries 146 | index = len(payload) 147 | payload[6] = (index&0xFF00) >> 8 148 | payload[7] = (index&0x00FF) 149 | for a in anniversaries: 150 | payload += list(bytes(a)) 151 | 152 | return payload 153 | 154 | # Takes lists of TimexAppointment, TimexTodo, TimexPhoneNumber 155 | # and TimexAnniversary objects. Makes DATA1 payload and splits 156 | # it up as required 157 | def makeDATA1(appts, todos, phones, anniversaries, appt_alarm=0xff): 158 | payload = makeDATA1payload(appts, todos, phones, anniversaries, appt_alarm=appt_alarm) 159 | data1packets = [] 160 | index = 0 161 | while (payload): 162 | index += 1 163 | data1packets += makepkg([0x61, index]+payload[:27]) 164 | payload = payload[27:] 165 | 166 | return data1packets 167 | 168 | # Returns number of packets required for DATA1 169 | def DATA1_num_packets(appts, todos, phones, anniversaries, appt_alarm=0xff): 170 | data = makeDATA1payload(appts, todos, phones, anniversaries, appt_alarm=appt_alarm) 171 | 172 | return ceil(len(data)/27) 173 | 174 | # Takes lists of TimexAppointment, TimexTodo, TimexPhoneNumber 175 | # and TimexAnniversary objects 176 | def makeDATA1completeBreakfast(appts, todos, phones, anniversaries, appt_alarm=0xff): 177 | payload = makeDATA1payload(appts, todos, phones, anniversaries, appt_alarm=appt_alarm) 178 | data1packets = [] 179 | index = 0 180 | while (payload): 181 | index += 1 182 | data1packets += makepkg([0x61, index]+payload[:27]) 183 | payload = payload[27:] 184 | 185 | return makeSTART2(index) + data1packets + makeEND1() 186 | 187 | # Pass an ID 1 or 2, a datetime object containing current time in this 188 | # timezone and 12 or 24 for time format 189 | def makeTZ(tzno, tztime, format): 190 | data = [tzno, 191 | tztime.hour, tztime.minute, 192 | tztime.month, tztime.day, tztime.year%100, 193 | tztime.weekday(), tztime.second] 194 | 195 | if format == 12: 196 | data += [1] 197 | else: 198 | data += [2] 199 | 200 | return makepkg([0x30]+data) 201 | 202 | def makeTZNAME(tzno, tzname): 203 | if len(tzname)>3: 204 | raise Exception("Time zone name too long!") 205 | elif len(tzname)<3: 206 | tzname += " "*(3-len(tzname)) 207 | 208 | data = [tzno] 209 | data+= str2timex(tzname) 210 | return makepkg([0x31]+data) 211 | 212 | def makeTIMETZ(tzno, tztime, timeformat, tzname, dateformat = 2): 213 | if len(tzname)>3: 214 | raise Exception("Time zone name too long!") 215 | elif len(tzname)<3: 216 | tzname += " "*(3-len(tzname)) 217 | 218 | data = [tzno] 219 | data += [tztime.second, tztime.hour, tztime.minute] 220 | data += [tztime.month, tztime.day, tztime.year%100] 221 | data += str2timex(tzname) 222 | data += [tztime.weekday()] 223 | 224 | if format == 12: 225 | data += [1] 226 | else: 227 | data += [2] 228 | 229 | data += [dateformat] 230 | return makepkg([0x32]+data) 231 | 232 | # Takes an alarm number and a sequence ID (1-5) 233 | def makeALARM(alarm, seq): 234 | data = [seq] 235 | data += [alarm.hour, alarm.minute, alarm.month, alarm.day] 236 | 237 | label = alarm.label 238 | if len(label)<8: # Min 8 chars, pad with space 239 | label +=" "*(8-len(label)) 240 | label = label[:8] # Max 8 chars 241 | data += str2timex(label) # NOT packed! 242 | 243 | if alarm.audible: 244 | data += [1] 245 | else: 246 | data += [0] 247 | 248 | pkgdata = makepkg([0x50]+data) 249 | 250 | return bytes(pkgdata) 251 | 252 | def makeSALARM(seq): 253 | return makepkg([0x70, 0, 0x61+seq, 0]) 254 | 255 | def makeBEEPS(hourly=0, button=0): 256 | data = [hourly, button] 257 | 258 | return makepkg([0x71]+data) 259 | -------------------------------------------------------------------------------- /pytimex/TimexData.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from ._helpers import * 3 | 4 | class WatchModel: 5 | def __init__(self, name="DL50", protocol=1): 6 | self.name = name 7 | self.protocol = protocol 8 | 9 | def __str__(self): 10 | return self.name 11 | 12 | DL50 = WatchModel('DL50', 1) # I think this uses the same protocol as the 70 13 | DL70 = WatchModel('DL70', 1) 14 | DL150 = WatchModel('DL150', 3) 15 | DL150s = WatchModel('DL150s', 4) 16 | 17 | 18 | # Month name lookup 19 | monthNamesAbbr = [ 20 | "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", 21 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 22 | ] 23 | 24 | class TimexAppointment: 25 | def __init__(self, month=0, day=0, time=0, label=""): 26 | self.month=month 27 | self.day=day 28 | self.time=time 29 | self.label=label 30 | 31 | def __str__(self): 32 | ctime = "{:02}:{:02}".format(int(self.time/4), (self.time&0x03)*15) 33 | return "Appointment on the {} of {} at {}, label \"{}\"".format( 34 | self.day, monthNamesAbbr[self.month], ctime, self.label) 35 | 36 | def __bytes__(self): 37 | data = [self.month&0xFF, self.day&0xFF, self.time&0xFF] 38 | data = data + pack4to3(str2timex(self.label)) 39 | data = [len(data)+1] + data 40 | return bytes(data) 41 | 42 | 43 | class TimexTodo: 44 | def __init__(self, prio=0, label=""): 45 | self.prio=prio 46 | self.label=label 47 | 48 | def __str__(self): 49 | p = "priority {}".format(self.prio) if self.prio else "no priority" 50 | return "Todo with {}, label \"{}\"".format( 51 | p, self.label) 52 | 53 | def __bytes__(self): 54 | data = [self.prio] 55 | data = data + pack4to3(str2timex(self.label)) 56 | data = [len(data)+1] + data 57 | return bytes(data) 58 | 59 | 60 | class TimexPhoneNumber: 61 | def __init__(self, number="1", label=""): 62 | self.number=number 63 | self.label=label 64 | 65 | def __str__(self): 66 | return "Phone number {}, label \"{}\"".format( 67 | self.number, self.label) 68 | 69 | def __bytes__(self): 70 | # TODO: This only works with numbers 10 digits or less, and no type indication 71 | # Convert to digits 72 | conv_table = { 73 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 74 | '8': 8, '9': 9, 'C': 10, 'F': 11, 'H': 12, 'P': 13, 'W': 14, ' ': 15 75 | } 76 | digits = [conv_table[x] for x in str(self.number)] 77 | # Pad with spaces 78 | t = [15]*12 # Make a "template" filled with char 15 79 | t[-2-len(digits):-2] = digits # Replace part of template 80 | # Smush it up 81 | data = [d[1]<<4|d[0] for d in zip(t[0::2], t[1::2]) ] 82 | 83 | data = data + pack4to3(str2timex(self.label)) 84 | data = [len(data)+1] + data 85 | 86 | return bytes(data) 87 | 88 | 89 | class TimexAnniversary: 90 | def __init__(self, month=1, day=1, label=""): 91 | self.month=month 92 | self.day=day 93 | self.label=label 94 | 95 | def __str__(self): 96 | return "Anniversary on the {} of {}, label \"{}\"".format( 97 | self.day, monthNamesAbbr[self.month], self.label) 98 | 99 | def __bytes__(self): 100 | data = [self.month&0xFF, self.day&0xFF] 101 | data = data + pack4to3(str2timex(self.label)) 102 | data = [len(data)+1] + data 103 | return bytes(data) 104 | 105 | 106 | class TimexTimezone: 107 | def __init__(self, offset=0, format=24, name=""): 108 | self.offset = offset 109 | self.format = format 110 | self.name = name 111 | 112 | @property 113 | def format(self): 114 | return self._format 115 | 116 | @format.setter 117 | def format(self, f): 118 | if not f in [12,24]: 119 | raise Exception("Time format must be 12 or 24 hours") 120 | self._format = f 121 | 122 | def __str__(self): 123 | return "Time zone with offset UTC{:+} named \"{}\", {} hour format".format(offset, name, format) 124 | 125 | 126 | class TimexAlarm: 127 | def __init__(self, hour=0, minute=0, month=0, day=0, label="", audible=True): 128 | self.hour = hour 129 | self.minute = minute 130 | self.month = month 131 | self.day = day 132 | self.label = label 133 | self.audible = audible 134 | 135 | def __str__(self): 136 | audible_str = "audible" if self.audible else "inaudible" 137 | 138 | if self.day==0 and self.month==0: 139 | return "Alarm at {:02d}:{:02d}, label \"{}\", {}".format( 140 | self.hour, self.minute, self.label, audible_str) 141 | 142 | if self.day==0: 143 | return "Alarm at {:02d}:{:02d} every day in {}, label \"{}\", {}".format( 144 | self.hour, self.minute, monthNamesAbbr[self.month], self.label, audible_str) 145 | 146 | if self.month==0: 147 | return "Alarm at {:02d}:{:02d} on the {}, label \"{}\", {}".format( 148 | self.hour, self.minute, self.day, self.label, audible_str) 149 | 150 | return "Alarm at {:02d}:{:02d} on the {} of {}, label \"{}\", {}".format( 151 | self.hour, self.minute, self.day, monthNamesAbbr[self.month], self.label, audible_str) 152 | 153 | 154 | class TimexData: 155 | def __init__(self, model=DL70): 156 | self.appointments = [] 157 | self.todos = [] 158 | self.phonenumbers = [] 159 | self.anniversaries = [] 160 | self.alarms = [] 161 | self.sendTime = False 162 | self.tz=[ 163 | TimexTimezone(0, 24, "utc"), 164 | TimexTimezone(-5, 12, "est") 165 | ] 166 | # Number of seconds to add to time when blasting, to compensate 167 | # for the time between building the packet and the watch 168 | # receiving it. 169 | self.secondsOffset=8 170 | self.model=model 171 | 172 | def setTimezone(self, tzno, offset, format, name): 173 | if tzno not in [1,2]: 174 | raise Exception("Time zone number must be 1 or 2!") 175 | 176 | if len(name)>3: 177 | raise Exception("Max length for time zone name is 3 characters!") 178 | 179 | self.tz[tzno-1] = TimexTimezone(offset, format, name) 180 | 181 | def addAppointment(self, appointment): 182 | self.appointments.append(appointment) 183 | 184 | def addNewAppointment(self, *args, **kwargs): 185 | new = TimexAppointment(*args, **kwargs) 186 | self.addAppointment(new) 187 | return new 188 | 189 | def delAppointment(self, appointment): 190 | self.appointments = [a for a in self.appointments if a != appointment] 191 | 192 | def addTodo(self, todo): 193 | self.todos.append(todo) 194 | 195 | def addNewTodo(self, *args, **kwargs): 196 | new = TimexTodo(*args, **kwargs) 197 | self.addTodo(new) 198 | return new 199 | 200 | def delTodo(self, todo): 201 | self.todos = [a for a in self.todos if a != todo] 202 | 203 | def addPhoneNumber(self, phonenumber): 204 | self.phonenumbers.append(phonenumber) 205 | 206 | def addNewPhoneNumber(self, *args, **kwargs): 207 | new = TimexPhoneNumber(*args, **kwargs) 208 | self.addPhoneNumber(new) 209 | return new 210 | 211 | def delPhoneNumber(self, phonenumber): 212 | self.phonenumbers = [a for a in self.phonenumbers if a != phonenumber] 213 | 214 | def addAnniversary(self, anniversary): 215 | self.anniversaries.append(anniversary) 216 | 217 | def addNewAnniversary(self, *args, **kwargs): 218 | new = TimexAnniversary(*args, **kwargs) 219 | self.addAnniversary(new) 220 | return new 221 | 222 | def delAnniversary(self, anniversary): 223 | self.anniversaries = [a for a in self.anniversaries if a != anniversary] 224 | 225 | def addAlarm(self, alarm): 226 | self.alarms.append(alarm) 227 | 228 | def addNewAlarm(self, *args, **kwargs): 229 | new = TimexAlarm(*args, **kwargs) 230 | self.addAlarm(new) 231 | return new 232 | 233 | def delAlarm(self, alarm): 234 | self.alarms = [a for a in self.alarms if a != alarm] 235 | 236 | def __bytes__(self): 237 | data = b'' 238 | 239 | data += bytes(makeSTART1(version=self.model.protocol)) 240 | 241 | if self.sendTime: 242 | now = datetime.datetime.utcnow() + datetime.timedelta(0, self.secondsOffset) 243 | tz1time = now+datetime.timedelta(hours=self.tz[0].offset) 244 | tz2time = now+datetime.timedelta(hours=self.tz[1].offset) 245 | if self.model.protocol == 1: 246 | data += bytes(makeTZ(1, tz1time, self.tz[0].format)) 247 | data += bytes(makeTZ(2, tz2time, self.tz[1].format)) 248 | data += bytes(makeTZNAME(1, self.tz[0].name)) 249 | data += bytes(makeTZNAME(2, self.tz[1].name)) 250 | elif self.model.protocol == 3 or self.model.protocol == 4: 251 | data += bytes(makeTIMETZ(1, tz1time, self.tz[0].format, self.tz[0].name)) 252 | data += bytes(makeTIMETZ(2, tz2time, self.tz[1].format, self.tz[1].name)) 253 | 254 | if ( 255 | len(self.appointments)>0 or 256 | len(self.todos)>0 or 257 | len(self.phonenumbers)>0 or 258 | len(self.anniversaries)>0 259 | ): 260 | if self.model.protocol == 1: 261 | data += bytes(makeSTART2(DATA1_num_packets(self.appointments, self.todos, self.phonenumbers, self.anniversaries))) 262 | data += bytes(makeDATA1(self.appointments, self.todos, self.phonenumbers, self.anniversaries)) 263 | data += bytes(makeEND1()) 264 | else: 265 | print("DATA payload not implemented for protocol {}".format(self.model.protocol)) 266 | 267 | # It's not necessary to send all alarms at once. Though this 268 | # will leave the alarms not explicitly overwritten. So it might 269 | ## be a good idea to do that. Or make it an option? 270 | if len(self.alarms)>0: 271 | i=1 272 | for a in self.alarms: 273 | data += makeALARM(a, i) 274 | i+=1 275 | if not a.audible and self.model.protocol == 1: 276 | data += makeSALARM(i) 277 | 278 | data += bytes(makeEND2()) 279 | 280 | return data 281 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | This document is based on [the protocol documentation by Tommy Johnson] 2 | (https://web.archive.org/web/20030803072005/http://csgrad.cs.vt.edu/~tjohnson/timex/). 3 | I have added and corrected some information. There may be more differences 4 | between watch versions, but most information for the model 70 should be 5 | correct. 6 | 7 | Some info also from http://www.toebes.com/Datalink/download.html. He 8 | has a lot of low level information about app programming and such. 9 | 10 | The watch I have tested most with is a model 70 which shows 786003 on 11 | boot. I'm guessing that's some kind of model number or software version. 12 | 13 | I also got a hold of a model 150 (802003), and is investing its protocol. 14 | Slightly different but has has the same general structure. Sound and app 15 | packets are not yet documented. 16 | 17 | 18 | ## Physical level 19 | 20 | Data is sent as a series of pulses. When sent using th CRT, each byte 21 | appears as 1-9 horizontal lines, with 2 bytes being sent each frame. A 22 | CRT draws its image sequentially as it receives the video signal, with 23 | each line fading shortly after being drawn, so to a light sensor it 24 | appears as a short pulse. LCDs in general receives an entire frame and 25 | draws all pixels almost at the same time, which is why LCDs do not work 26 | for data transfer. 27 | 28 | Timex solved this problem by offering a "Notebook Adapter" which 29 | essentialy is a serially attached unit which transcodes data into the 30 | appropriate pulses for the watch to receive. This gives us a very nice 31 | advantage: it is super simple to log data from the program. 32 | 33 | Bytes are sent least significant bit first, one start bit, no stop bit. A 34 | one is indicated by the absence of a pulse, a zero is a pulse. This means 35 | the byte 0x5a would look like pnpnppnpn (where p indicates a pulse, and n 36 | the absence of a pulse). 37 | 38 | Some measurements were done by probing the video signal. Pulses are 39 | approximately 32 µs long. Pulses are sent at an interval of approximately 40 | 480 µs. Some configuration files mentioned 2048 baud, which would mean 41 | approximately 488 µs. So this sort of makes sense. 42 | 43 | Data transfer using CRT is done at 640x480@60Hz. At this resolution and 44 | frequency, the horizontal refresh frequency is 31.46875 kHz, so one line 45 | is drawn approximately every 31.78 µs. Using these timings, we can 46 | quantize the above measurements and conclude that one bit is one line and 47 | a bit is sent every 15 lines. The watch seems to be a bit flexible on 48 | this though. 49 | 50 | Data bytes are separated by approximately 2 ms, and packets are separated 51 | by approximately 240 ms. The interpacket delay is also present after each 52 | block of synchronization bytes (i.e. after all 0x55 is sent and after all 53 | 0xAA are sent). The 2 ms interbyte delay happens to be the same time it 54 | takes to send two bytes at 9600 baud, in this case the computer sending 55 | the data byte and receiving confirmation it was sent. 56 | 57 | 58 | ## Synchronization 59 | 60 | When initiating transfer, first 200 bytes of 0x55 is sent. During this 61 | time the watch will beep and give the user some time to align the watch. 62 | The exact number here is not important. 63 | 64 | After that 50 bytes of 0xAA is sent. The original protocol documentation 65 | says 40 bytes, but I got 50 bytes from the software and I have not 66 | verified if the exact number is important. 67 | 68 | 69 | ## Strings 70 | 71 | Strings are encoded in a special charset, which is referencet to as 72 | timexscii, timex charset or similar. 73 | 74 | The character set is: 75 | ``` 76 | 0123456789 77 | abcdefghijklmnopqrstuvwxyz 78 | !"#$%&'()*+,-./:\[divide]=[bell symbol]? 79 | [underscore][underscored check mark][left arrow] 80 | [right arrow][big square][small square] 81 | ``` 82 | 83 | The small square can be used only on unpacked strings, since on packed 84 | strings it is interpreted as a string terminator. 85 | 86 | Since only 6 bits are used per character, the 24 bits of 4 characters can 87 | be packed into 3 bytes. 88 | 89 | First byte: 90 | - High 2 bits contin low bits of second character 91 | - Low 6 bits contain first character 92 | 93 | Second byte: 94 | - High 4 bits contain low bits of third character 95 | - Low 4 bits contain high bits of second character 96 | 97 | Third byte: 98 | - High 6 bits contain last character 99 | - Low 2 bits contain high bits of third character 100 | 101 | Strings are terminated by a character with all ones (0x3F). If there are 102 | any 0 bytes after packing, these are removed. 103 | 104 | If the above explanation, maybe some pseudo code can clear it up: 105 | 106 | ``` 107 | byte0 = (ch2&0x03)<<6) | (ch1&0x3F) 108 | byte1 = (ch3&0x0F)<<4) | ((ch2>>2)&0x0F) 109 | byte2 = (ch4&0x3F)<<2) | ((ch3&0x30)>>4) 110 | ``` 111 | 112 | Packed string max length is 15 characters + terminator. If there is no 113 | terminator in the 16 characters, the watch will start displaying its own 114 | memory. 115 | 116 | The Windows help file states that the 150 and 150s supports up to 31 117 | characters + terminator. I have not yet tested this. 118 | 119 | ## Data packets 120 | 121 | Data is separated into packets. Each packet has a framing consisting of 122 | packet length, data type and checksum as follows: 123 | 124 | | Byte no. | Description | 125 | | -------- | -------------------------------- | 126 | | 1 | Packet lengh (including framing) | 127 | | 2 | Packet type | 128 | | 3->len-2 | Payload (if any) | 129 | | len-1 | High byte of checksum | 130 | | len | Low byte of checksum | 131 | 132 | Checksum is CRC-16/ARC. 133 | 134 | When describing packet contents below, only the payload bytes are 135 | considered. Therefore, "byte 1" would mean the third byte of the packet. 136 | 137 | 138 | ### Packet types 139 | 140 | Note that names are made up and not official Timex names. 141 | 142 | List is ordered by in which order the watch expects the packets. I will 143 | probably add more info on order later. 144 | 145 | | ID | Name | Description | 146 | | ---- | ------ | ----------------------------------- | 147 | | 0x20 | START1 | Data transfer start | 148 | | 0x30 | TIME | Time information | 149 | | 0x31 | TZNAME | Time zone name | 150 | | 0x32 | TIMETZ | Time information and time zone name | 151 | | 0x60 | START2 | Marks start of DATA1 packets | 152 | | 0x61 | DATA1 | Contains lots of data | 153 | | 0x62 | END1 | Marks end of DATA1 packets | 154 | | 0x50 | ALARM | Alarm data | 155 | | 0x70 | SALARM | Sent after silent alarms | 156 | | 0x21 | END2 | End of data transfer | 157 | 158 | Packets 0x90, 0x91, 0x92, 0x93 and 0x71 are used for data in 159 | version 3. These are only somewhat documented here. 160 | 161 | | ID | Name | Description | 162 | | ---- | ------ | ----------------------------------- | 163 | | 0x90 | START3 | Marks start and type of following packets | 164 | | 0x91 | DATA | | 165 | | 0x92 | END | | 166 | | 0x93 | CLEAR | | 167 | | 0x71 | BEEPS | | 168 | 169 | 170 | 171 | ### 0x20 - START1 172 | 173 | Versions: All 174 | 175 | | Byte | Description | 176 | | ---- | ----------- | 177 | | 1 | Always 0x00 | 178 | | 2 | Always 0x00 | 179 | | 3 | Version | 180 | 181 | Version is 0x01 for model 70, 0x03 for model 150 and 0x04 for model 150s. 182 | 183 | Example packet: 0x07 0x20 0x00 0x00 0x01 0xc0 0x7f 184 | 185 | 186 | ### 0x30 - TIME 187 | 188 | Versions: 1 189 | 190 | | Byte | Description | 191 | | ---- | -------------------------------- | 192 | | 1 | Timezone ID (1 or 2) | 193 | | 2 | Hour | 194 | | 3 | Minute | 195 | | 4 | Month | 196 | | 5 | Day of month | 197 | | 6 | Year (mod 100) | 198 | | 7 | Day of week (0=monday, 6=sunday) | 199 | | 8 | Seconds | 200 | | 9 | 12h format (1) or 24h format (2) | 201 | 202 | 203 | ### 0x31 - TZNAME 204 | 205 | Versions: 1 206 | 207 | | Byte | Description | 208 | | ---- | -------------------------------- | 209 | | 1 | Timezone ID (1 or 2) | 210 | | 2 | Character 1 of timezone name | 211 | | 3 | Character 2 of timezone name | 212 | | 4 | Character 3 of timezone name | 213 | 214 | Insert spaces on unused characters. 215 | 216 | Example: 0x02 0x0e 0x1c 0x1d - timezone 2 named EST 217 | 218 | 219 | ### 0x32 - TIMETZ 220 | 221 | Versions: 3, 4 222 | 223 | Combination of time packet and time zone name packet. Also includes 224 | information on date format. For models 150 and 150s. 225 | 226 | | Byte | Description | 227 | | ---- | -------------------------------- | 228 | | 1 | Timezone ID (1 or 2) | 229 | | 2 | Second | 230 | | 3 | Hour | 231 | | 4 | Minute | 232 | | 5 | Month | 233 | | 6 | Day of month | 234 | | 7 | Year (mod 100) | 235 | | 8 | Character 1 of timezone name | 236 | | 9 | Character 2 of timezone name | 237 | | 10 | Character 3 of timezone name | 238 | | 11 | Day of week (0=monday, 6=sunday) | 239 | | 12 | 12h format (1) or 24h format (2) | 240 | | 13 | Date format | 241 | 242 | Date format: 243 | * 1: DD-MM-YY 244 | * 2: YY-MM-DD 245 | * 3: Accepted, but gives XX-MM-DD where XX is random data 246 | 247 | 248 | ### 0x60 - START2 249 | 250 | | Byte | Description | 251 | | ---- | --------------------------------- | 252 | | 1 | Number of DATA1 packets to follow | 253 | 254 | 255 | ### 0x61 - DATA1 256 | 257 | | Byte | Description | 258 | | ---- | ---------------------------------- | 259 | | 1 | Sequence ID (starts at 1) | 260 | | 2->3 | Start index of appointments | 261 | | 4->5 | Start index of TODOs | 262 | | 6->7 | Start index of phone numbers | 263 | | 8->9 | Start index of anniversaries | 264 | | 10 | Number of appointments | 265 | | 11 | Number of TODOs | 266 | | 12 | Number of phone numbers | 267 | | 13 | Number of anniversaries | 268 | | 14 | Year of the first appointment entry| 269 | | 15 | Early alarm, in 5 minute intervals | 270 | 271 | Sequence ID is incremented for each DATA1 packet sent. 272 | 273 | Indices are counted zero inexed from first address byte (2). This means 274 | the first data is always located at index 0x0e. Indexes are given in 275 | MSB first, LSB second. 276 | 277 | Byte 14 is the year of the first occurring appointment entry. The watch 278 | probably uses this for computing the day of the week an appointment 279 | occurs on. The watch most likely implicitly assumes that the 280 | appointments do not span more than a year into the future, and possibly 281 | that the entries are uploaded in chronological order. 282 | 283 | Byte 15 indicates how long before appointments, in 5 minute intervals, 284 | the alarm will sound. Set to 0xFF for no alarm 285 | 286 | It seems the maximum length of DATA1 packets sent by original software is 287 | 32 (0x20) bytes. Payloads of packets 2 and forward are just concatenated 288 | to the first, i.e. no header is added. Header and checksum are not counted 289 | against start indices. 290 | 291 | The following data in the payload are records of the following 292 | format: 293 | 294 | These 4 record types are found in DATA1 packets: 295 | 296 | 297 | #### Appointment record 298 | 299 | | Byte | Description | 300 | | ------ | ---------------------------------- | 301 | | 1 | Record length | 302 | | 2 | Month | 303 | | 3 | Day | 304 | | 4 | Time (see below) | 305 | | 5->len | Packed string | 306 | 307 | Time is encoded in 15 minute intervals since midnight, such that 08:45 is 308 | 8*4+3=35. 309 | 310 | Model 70: If a value of 90 or higher is used, time will not wrap. 90 will 311 | show as 24:00, 91 as 24:15, and 255 as 63:45. This will probably not work 312 | with alarms. Month and day are not boundary checked either. 313 | 314 | 315 | #### Todo record 316 | 317 | | Byte | Description | 318 | | ------ | ---------------------------------- | 319 | | 1 | Record length | 320 | | 2 | Priority (0 or 1-5) | 321 | | 3->len | Packed string | 322 | 323 | Original software only sends priority 0 to 5. The number actually 324 | represents a timexscii character, so any priority up to 63 can be used. 325 | For 0, no priority is shown on the watch. If you want to show "PRI - 0" 326 | on the watch, you can set the priority to 64. 327 | 328 | 329 | #### Phone number record 330 | 331 | | Byte | Description | 332 | | ------ | ---------------------------------- | 333 | | 1 | Record length | 334 | | 2-7 | Phone number (BCD, 2 digits per byte, little endian) | 335 | | 8->len | Packed string | 336 | 337 | Unused digits in phone numbers are set to 0xF. 338 | 339 | In the original software, you can set a "type" of number. This can only 340 | be done for numbers 11 digits or shorter, and the last digit is replaced 341 | by a character from the table below. 342 | 343 | | Type | Description | 344 | | ---- | ---------------------- | 345 | | 0xA | Cellular (C) | 346 | | 0xB | Fax (F) | 347 | | 0xC | Home (H) | 348 | | 0xD | Pager (P) | 349 | | 0xE | Work (W) | 350 | | 0xF | None (No letter shown) | 351 | 352 | It seems possible to use these character at any position in the phone 353 | number, if you want. 354 | 355 | If phone number is 10 digits or shorter, the last two digits are unused. 356 | This is done to reserve space for the type, and a space between number 357 | and type. If the number is 11 digits long, the space between number and 358 | type is used. If the number is 12 digits long, the type is not included 359 | and that space is used for a number instead. 360 | 361 | Another peculiarity is that you can send multiple numbers for the same 362 | name by completely omitting the name, not even sendig a string 363 | terminator, on successive messages. These packets will always be 7 bytes 364 | long. 365 | 366 | 367 | #### Anniversary record 368 | 369 | | Byte | Description | 370 | | ------ | ---------------------------------- | 371 | | 1 | Record length | 372 | | 2 | Month | 373 | | 3 | Day | 374 | | 4->len | Packed string | 375 | 376 | 377 | ### 0x62 - END1 378 | 379 | No payload. Marks end of DATA1 packets. 380 | 381 | 382 | ### 0x50 - ALARM 383 | 384 | Versions: 1, 3 385 | 386 | | Byte | Description | 387 | | ----- | ---------------------------------- | 388 | | 1 | Alarm ID (starts at 1) | 389 | | 2 | Hour | 390 | | 3 | Minute | 391 | | 4 | Month (0 for every month) | 392 | | 5 | Day (0 for every day) | 393 | | 6->13 | Label, unpacked | 394 | | 14 | 1 if audible, otherwise 0 | 395 | 396 | The original software always sends all of the alarms. I suppose that's 397 | good to keep the alarms guaranteed in synchronization. 398 | 399 | Model 70: It seems to be possible to send only some alarms, if you want. 400 | The alarms that are not sent are unchanged. Sending alarms with index 401 | greater than 5 might make the watch hang. Don't do this. 402 | 403 | After silent alarms, send a 0x70, writing 0 to address Alarm ID + 0x61. 404 | For example, for alarm 3 send 0x70 with payload 0x00 0x64 0x00. I think 405 | this is sent to patch some firmware error in the watches. To be 406 | investigated! 407 | 408 | Model 150: 409 | 410 | 411 | ### 0x70 - MEM 412 | 413 | Versions: 1, 414 | 415 | Didn't know what to call this. Sent after silent alarms on version 1, 416 | not sent on version 3. 417 | 418 | | Byte | Description | 419 | | ---- | ---------------------------------- | 420 | | 1 | High address | 421 | | 2 | Low address | 422 | | 3+ | Data to write | 423 | 424 | According to documentation at http://www.toebes.com/Datalink/download.html, 425 | this is a package to write data to a specific address in memory. Any number 426 | of bytes may be written. 427 | 428 | 429 | ### 0x71 - BEEPS 430 | 431 | Versions: 1?, 3 432 | 433 | | Byte | Description | 434 | | ---- | ---------------------------------- | 435 | | 1 | Hourly chimes (0 off, else on) | 436 | | 2 | Button beeps (0 off, else on) | 437 | 438 | TODO: Test this 439 | 440 | 441 | ### 0x21 - END2 442 | 443 | No payload 444 | 445 | --------------------------------------------------------------------------------