From 1cd3fd38863469d741929e66e6b477850f551da1 Mon Sep 17 00:00:00 2001 From: Antti Huhtala Date: Sun, 3 Apr 2022 16:34:44 +0300 Subject: [PATCH] Initial date format support for 150/150s and initial wristapp support. --- PROTOCOL.md | 93 +++++++++++++++++++++++++++++++++++++------- pytimex/TimexData.py | 66 +++++++++++++++++++++++++++---- pytimex/_helpers.py | 27 ++++++++++++- 3 files changed, 164 insertions(+), 22 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 49ceceb..d2fd1d4 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -321,24 +321,24 @@ Versions: 1 No payload. Marks end of DATA1 packets. -### 0x93 - subtype 1 - CLEAR_EEPROM +### 0x93 - Subtype 1 - CLEAR_EEPROM Versions: 3, 4 | Byte | Description | | ---- | ------------------------ | -| 1 | subtype code, 1 = EEPROM | +| 1 | Subtype code, 1 = EEPROM | Clear EEPROM meta data from watch SRAM. Watch forgets how to interpret -data in EEPROM. However, actual data in EEPROM remains in place. +data in EEPROM. However, actual data in EEPROM is not touched. -### 0x90 - subtype 1 - START_EEPROM +### 0x90 - Subtype 1 - START_EEPROM Versions: 3, 4 | Byte | Description | | ----- | ----------------------------------------------- | -| 1 | subtype code, 1 = EEPROM | +| 1 | Subtype code, 1 = EEPROM | | 2 | Number of DATA_EEPROM packets to be transmitted | | 3->4 | Address to start of appointment data | | 5->6 | Address to start of todo data | @@ -353,6 +353,12 @@ Versions: 3, 4 Gets the watch ready to receive a sequence of EEPROM data. +The addresses in 3->4, 5->6, 7->8 and 9->10 point to where those particular +pieces of data begins at on the EEPROM. The addresses are given so that the +first byte is the MSB of the address while the second byte is the LSB. The +beginning of the EEPROM is denoted by address 0x0236. The addressing is in +bytes. + Byte 15 is the year of the first occurring appointment entry. The watch uses this for computing the day of the week an appointment occurs on. The watch assumes that the entries are uploaded in chronological order, @@ -361,32 +367,35 @@ and that the gap between adjacent appointments is less than a year. There appears to be a bug in how the watch calculates days of the week for the appointments. Wrapping around from the final appointment back to the first will show an incorrect day of the week for all appointments. -Re-enteering the appointment mode will again calculate the days correctly. +Re-entering the appointment mode, or scanning through the dates will again +calculate the days correctly. Byte 16 indicates how long before appointments, in 5 minute intervals, the alarm will sound. Set to 0xFF for no alarm -### 0x91 - subtype 1 - DATA_EEPROM +### 0x91 - Subtype 1 - DATA_EEPROM Versions: 3, 4 | Byte | Description | | ------ | ---------------------------------- | -| 1 | subtype code, 1 = EEPROM | +| 1 | Subtype code, 1 = EEPROM | | 2 | Sequence ID (starts at 1) | | 3->len | EEPROM payload data | -The data in the payload consists of records in the EEPROM data record -formats. The maximum length of the payload is 32 bytes, leading to a +The maximum length of the payload is 32 bytes, leading to a maximum packet size of 38 bytes. -### 0x92 - subtype 1 - END_EEPROM +The data in the payload consists of records in the EEPROM data record +formats. + +### 0x92 - Subtype 1 - END_EEPROM Versions: 3, 4 | Byte | Description | | ---- | ------------------------ | -| 1 | subtype code, 1 = EEPROM | +| 1 | Subtype code, 1 = EEPROM | Ends the EEPROM transmission sequence. @@ -472,6 +481,62 @@ long. | 4->len | Packed string | +### 0x93 - Subtype 2 - CLEAR_WRISTAPP + +Versions: 3, 4 + +| Byte | Description | +| ---- | -------------------------- | +| 1 | Subtype code, 2 = WRISTAPP | + +Clear Wristapp from memory. + +### 0x90 - Subtype 2 - START_WRISTAPP + +Versions: 3, 4 + +| Byte | Description | +| ----- | ------------------------------------------------- | +| 1 | Subtype code, 2 = WRISTAPP | +| 2 | Number of DATA_WRISTAPP packets to be transmitted | +| 3 | Value written to address 0x010e in SRAM | + +Starts a transfer sequence to upload a new Wristapp. + +Value put in byte 3 will be written to 0x010e in watch SRAM. The meaning of +this is unknown. Original Timex software appears to always write a 0x01. + +It appears writing any other value to byte 3 will cause the watch to accept the +wristapp in the transfer, but it will not launch it. + +### 0x91 - Subtype 1 - DATA_WRISTAPP + +Version 3, 4 + +| Byte | Description | +| ------ | ---------------------------------- | +| 1 | Subtype code, 2 = WRISTAPP | +| 2 | Sequence ID (starts at 1) | +| 3->len | Wristapp payload data | + +The maximum length of the payload is 32 bytes, leading to a +maximum packet size of 38 bytes. + +The data in the payload consists of data bytes to be written to SRAM +starting from address 0x0110. There appears to be no protection against +overflow. Writing past the Wristapp area will first corrupt the sound +area and then continue onto the upper RAM area. + +### 0x92 - Subtype 2 - END_WRISTAPP + +Versions: 3, 4 + +| Byte | Description | +| ---- | -------------------------- | +| 1 | Subtype code, 2 = Wristapp | + +Ends the Wristapp transmission sequence. + ### 0x50 - ALARM Versions: 1, 3 @@ -504,7 +569,7 @@ no 0x70 packets need to be sent. ### 0x70 - MEM -Versions: 1, 3, 4, 9 +Versions: 1, 3?, 4?, 9? Didn't know what to call this. Sent after silent alarms on version 1, not sent on version 3 by the original Timex software. Seems to be sent @@ -523,7 +588,7 @@ of bytes may be written. ### 0x71 - BEEPS -Versions: 1?, 3 +Versions: 1?, 3?, 4? | Byte | Description | | ---- | ---------------------------------- | diff --git a/pytimex/TimexData.py b/pytimex/TimexData.py index d896ab1..7042830 100644 --- a/pytimex/TimexData.py +++ b/pytimex/TimexData.py @@ -13,7 +13,14 @@ DL50 = WatchModel('DL50', 1) # I think this uses the same protocol as the 70 DL70 = WatchModel('DL70', 1) DL150 = WatchModel('DL150', 3) DL150s = WatchModel('DL150s', 4) +DLIRONMAN = WatchModel('DLIRONMAN', 9) +MMDDYY_DASHES = 0 +DDMMYY_DASHES = 1 +YYMMDD_DASHES = 2 +MMDDYY_DOTS = 4 +DDMMYY_DOTS = 5 +YYMMDD_DOTS = 6 # Month name lookup monthNamesAbbr = [ @@ -104,9 +111,10 @@ class TimexAnniversary: class TimexTimezone: - def __init__(self, offset=0, format=24, name=""): + def __init__(self, offset=0, format=24, name="", dateformat=MMDDYY_DASHES): self.offset = offset self.format = format + self.dateformat = dateformat self.name = name @property @@ -119,8 +127,27 @@ class TimexTimezone: raise Exception("Time format must be 12 or 24 hours") self._format = f + @property + def dateformat(self): + return self._dateformat + + @dateformat.setter + def dateformat(self, f): + if not f in [0,1,2,4,5,6]: + raise Exception("Date format not valid") + self._dateformat = f + def __str__(self): - return "Time zone with offset UTC{:+} named \"{}\", {} hour format".format(offset, name, format) + dateformatstring = { + 0: "MM-DD-YY", + 1: "DD-MM-YY", + 2: "YY-MM-DD", + 4: "MM.DD.YY", + 5: "DD.MM.YY", + 6: "YY.MM.D" + }.get(self.dateformat, "invalid dateformat") + + return "Time zone with offset UTC{:+} named \"{}\", {} hour format, date format {}".format(self.offset, self.name, self.format, dateformatstring) class TimexAlarm: @@ -150,6 +177,17 @@ class TimexAlarm: return "Alarm at {:02d}:{:02d} on the {} of {}, label \"{}\", {}".format( self.hour, self.minute, self.day, monthNamesAbbr[self.month], self.label, audible_str) +class TimexWristapp: + def __init__(self, filename=None, databytes=None): + if databytes != None: + self.databytes = databytes + elif filename != None: + f = open(filename,"rb") + self.databytes = list(f.read()) + f.close() + else: + raise Exception("You need to specify either a file name or the data") + class TimexData: def __init__(self, model=DL70): @@ -163,20 +201,21 @@ class TimexData: TimexTimezone(0, 24, "utc"), TimexTimezone(-5, 12, "est") ] + self.wristapp = None # Number of seconds to add to time when blasting, to compensate # for the time between building the packet and the watch # receiving it. self.secondsOffset=8 self.model=model - def setTimezone(self, tzno, offset, format, name): + def setTimezone(self, tzno, offset, format, name, dateformat=MMDDYY_DASHES): if tzno not in [1,2]: raise Exception("Time zone number must be 1 or 2!") if len(name)>3: raise Exception("Max length for time zone name is 3 characters!") - self.tz[tzno-1] = TimexTimezone(offset, format, name) + self.tz[tzno-1] = TimexTimezone(offset, format, name, dateformat=dateformat) def addAppointment(self, appointment): self.appointments.append(appointment) @@ -233,6 +272,12 @@ class TimexData: def delAlarm(self, alarm): self.alarms = [a for a in self.alarms if a != alarm] + def setWristapp(self, filename=None, databytes=None): + self.wristapp = TimexWristapp(filename=filename, databytes=databytes) + + def delWristapp(self): + self.wristapp = None + def __bytes__(self): data = b'' @@ -248,9 +293,10 @@ class TimexData: data += bytes(makeTZNAME(1, self.tz[0].name)) data += bytes(makeTZNAME(2, self.tz[1].name)) elif self.model.protocol == 3 or self.model.protocol == 4: - data += bytes(makeTIMETZ(1, tz1time, self.tz[0].format, self.tz[0].name)) - data += bytes(makeTIMETZ(2, tz2time, self.tz[1].format, self.tz[1].name)) - + data += bytes(makeTIMETZ(1, tz1time, self.tz[0].format, self.tz[0].name, self.tz[0].dateformat)) + data += bytes(makeTIMETZ(2, tz2time, self.tz[1].format, self.tz[1].name, self.tz[1].dateformat)) + else: + print("Time upload not implemented for protocol {}".format(self.model.protocol)) if ( len(self.appointments)>0 or len(self.todos)>0 or @@ -266,6 +312,12 @@ class TimexData: else: print("DATA payload not implemented for protocol {}".format(self.model.protocol)) + if self.wristapp != None: + if self.model.protocol == 3 or self.model.protocol == 4: + data += bytes(makeDATA_WRISTAPPcompleteBreakfast(self.wristapp)) + else: + print("Wristapps not supported for protocol {}".format(self.model.protocol)) + # It's not necessary to send all alarms at once. Though this # will leave the alarms not explicitly overwritten. So it might ## be a good idea to do that. Or make it an option? diff --git a/pytimex/_helpers.py b/pytimex/_helpers.py index 3db89e8..d74acc2 100644 --- a/pytimex/_helpers.py +++ b/pytimex/_helpers.py @@ -276,7 +276,7 @@ def makeDATA_EEPROMcompleteBreakfast(appts, todos, phones, anniversaries, appt_a num_packets = ceil(len(payload)/32) # CLEAR_EEPROM - eeprompackets = makepkg([0x03, 0x01]) + eeprompackets = makepkg([0x93, 0x01]) # START_EEPROM eeprompackets += makepkg([0x90, 0x01, num_packets] + header) @@ -293,6 +293,31 @@ def makeDATA_EEPROMcompleteBreakfast(appts, todos, phones, anniversaries, appt_a return eeprompackets +# Takes a TimexWristapp object. +# Returns the CLEAR_WRISTAPP, START_WRISTAPP, DATA_WRISTAPP and STOP_WRISTAPP packets consistent with the data +def makeDATA_WRISTAPPcompleteBreakfast(wristapp): + payload = wristapp.databytes + + num_packets = ceil(len(payload)/32) + + # CLEAR_WRISTAPP + wristapppackets = makepkg([0x93, 0x02]) + + # START_WRISTAPP + wristapppackets += makepkg([0x90, 0x02, num_packets, 0x01]) + + # DATA_WRISTAPP packets + index = 0 + while (payload): + index += 1 + wristapppackets += makepkg([0x91, 0x02, index]+payload[:32]) + payload = payload[32:] + + # END_WRISTAPP + wristapppackets += makepkg([0x92, 0x02]) + + return wristapppackets + # Pass an ID 1 or 2, a datetime object containing current time in this # timezone and 12 or 24 for time format def makeTZ(tzno, tztime, format):