Adding python code

This commit is contained in:
Simon Larsson
2020-05-29 21:34:02 +02:00
parent c830da0809
commit 9073271cd5
7 changed files with 665 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# Data Blaster for Timex
# Will not work with original adapter due to timing issues
import serial
import sys
import time
class Blaster:
def __init__(self, portname, syncbuflen=16):
self.port = serial.Serial(portname, 9600, timeout=0.1)
time.sleep(2)
self.sent = []
self.syncbuflen = syncbuflen
def identify(self):
self.port.write(b'x')
indata = self.port.read(1)
if indata != b'x':
raise Exception("Transceiver not detected! (x error)")
return False
self.port.write(b'?')
time.sleep(0.2)
indata = self.port.read(5)
if indata != b'M764\0':
print (indata)
raise Exception("Transceiver not detected! (? error)")
return False
return True
def _checksync(self):
# print("Sent: "+str(self.sent))
inbytes = self.port.read(int(self.syncbuflen/2))
if len(inbytes) == 0:
return
# print("Syncing {} byte(s)".format(len(inbytes)))
for byt in inbytes:
if byt != self.sent[0]:
print("Expected {}, got {}".format(self.sent[0], byt))
raise Exception("Out of sync!")
self.sent.pop(0)
def send_sync(self, times55sync=128, timesAAsync=50):
for sync in range(times55sync):
self.blast(0x55)
for sync in range(timesAAsync):
self.blast(0xAA)
def blast(self, data):
self.port.write(bytes([data]))
self.sent.append(data)
if len(self.sent)>self.syncbuflen:
self._checksync()
def flush(self):
while len(self.sent)>0:
self._checksync()
if __name__ == "__main__":
portname = sys.argv[1]
b = Blaster(portname, syncbuflen=16)
if not b.identify():
print("Could not verify adapter :(")
sys.exit(-1)
b.send_sync()
for p in range(10):
b.blast(0x03)
b.blast(0x02)
b.blast(0x01)
b.flush()
+229
View File
@@ -0,0 +1,229 @@
import datetime
from ._helpers import *
monthNamesAbbr = [
"<unknown>", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]
class TimexAppointment:
def __init__(self, month=0, day=0, time=0, label=""):
self.month=month
self.day=day
self.time=time
self.label=label
def __str__(self):
ctime = "{:02}:{:02}".format(int(self.time/4), (self.time&0x03)*15)
return "Appointment on the {} of {} at {}, label \"{}\"".format(
self.day, monthNamesAbbr[self.month], ctime, self.label)
def __bytes__(self):
data = [self.month&0xFF, self.day&0xFF, self.time&0xFF]
data = data + pack4to3(str2timex(self.label))
data = [len(data)+1] + data
return bytes(data)
class TimexTodo:
def __init__(self, prio=1, label=""):
self.prio=prio
self.label=label
def __str__(self):
return "Todo with priority {}, label \"{}\"".format(
self.prio, self.label)
def __bytes__(self):
data = [self.prio]
data = data + pack4to3(str2timex(self.label))
data = [len(data)+1] + data
return bytes(data)
class TimexPhoneNumber:
def __init__(self, number=1, label=""):
self.number=number
self.label=label
def __str__(self):
return "Phone number {}, label \"{}\"".format(
self.number, self.label)
def __bytes__(self):
# Encoded as four bits per digit and LSD first, always 10 digits
digits = [ord(x)-ord('0') for x in "{:010d}".format(self.number)[::-1]]
data = [d[0]<<4|d[1] for d in zip(digits[0::2], digits[1::2]) ]
data = data + pack4to3(str2timex(self.label))
data = [len(data)+1] + data
return bytes(data)
class TimexAnniversary:
def __init__(self, month=0, day=0, label=""):
self.month=month
self.day=day
self.label=label
def __str__(self):
return "Anniversary on the {} of {}, label \"{}\"".format(
self.day, monthNamesAbbr[self.month], self.label)
def __bytes__(self):
data = [self.month&0xFF, self.day&0xFF]
data = data + pack4to3(str2timex(self.label))
data = [len(data)+1] + data
return bytes(data)
class TimexTimezone:
def __init__(self, offset, format, name):
self.offset = offset
self.format = format
self.name = name
class TimexAlarm:
def __init__(self, hour=0, minute=0, month=0, day=0, label="", audible=True):
self.hour = hour
self.minute = minute
self.month = month
self.day = day
self.label = label
self.audible = audible
self.seq=0
def __bytes__(self):
# TODO: Adapt for 150 series
data = [self.seq] # TODO: Should be alarm sequence ID! (1-5)
data += [self.hour, self.minute, self.month, self.day]
label = self.label
if len(label)<8: # Min 8 chars, pad with space
label +=" "*(8-len(label))
label = label[:8] # Max 8 chars
data += str2timex(label)
if self.audible:
data += [1]
else:
data += [0]
pkgdata = makepkg([0x50]+data)
if not self.audible:
data = [0, 0x61+self.seq, 0]
pkgdata += makepkg([0x70]+data)
return bytes(pkgdata)
class TimexData:
def __init__(self):
self.appointments = []
self.todos = []
self.phonenumbers = []
self.anniversaries = []
self.alarms = []
self.sendTime = False
self.tz=[
TimexTimezone(0, 24, "utc"),
TimexTimezone(-5, 12, "est")
]
def setTimezone(self, tzno, offset, format, name):
if tzno not in [1,2]:
raise Exception("Time zone number must be 1 or 2!")
# TODO: Move these checks to time zone class
if format not in [12,24]:
raise Exception("Time format must be 12 or 24!")
if len(name)>3:
raise Exception("Max length for time zone name is 3 characters!")
self.tz[tzno-1] = TimexTimezone(offset, format, name)
def addAppointment(self, appointment):
self.appointments.append(appointment)
def addNewAppointment(self, month=0, day=0, time=0, label=""):
new = TimexAppointment(month, day, time, label)
self.addAppointment(new)
return new
def delAppointment(self, appointment):
self.appointments = [a for a in self.appointments if a != appointment]
def addTodo(self, todo):
self.todos.append(todo)
def addNewTodo(self, prio=1, label=""):
new = TimexTodo(prio, label)
self.addTodo(new)
return new
def delTodo(self, todo):
self.todos = [a for a in self.todos if a != todo]
def addPhoneNumber(self, phonenumber):
self.phonenumbers.append(phonenumber)
def addNewPhoneNumber(self, number=1, label=""):
new = TimexPhoneNumber(number, label)
self.addPhoneNumber(new)
return new
def delPhoneNumber(self, phonenumber):
self.phonenumbers = [a for a in self.phonenumbers if a != phonenumber]
def addAnniversary(self, anniversary):
self.anniversaries.append(anniversary)
def addNewAnniversary(self, month=0, day=0, label=""):
new = TimexAnniversary(month, day, label)
self.addAnniversary(new)
return new
def delAnniversary(self, anniversary):
self.anniversaries = [a for a in self.anniversaries if a != anniversary]
def addAlarm(self, alarm):
self.alarms.append(alarm)
def addNewAlarm(self, hour=0, minute=0, month=0, day=0, label="", audible=True):
new = TimexAlarm(hour, minute, month, day, label, audible)
self.addAlarm(new)
return new
def delAlarm(self, alarm):
self.alarms = [a for a in self.alarms if a != alarm]
def __bytes__(self):
data = b''
data += bytes(makeSTART1())
if self.sendTime:
now = datetime.datetime.utcnow()
tz1time = now+datetime.timedelta(hours=self.tz[0].offset)
tz2time = now+datetime.timedelta(hours=self.tz[1].offset)
data += bytes(makeTZ(2, tz2time, self.tz[1].format))
data += bytes(makeTZ(1, tz1time, self.tz[0].format))
data += bytes(makeTZNAME(1, self.tz[0].name))
data += bytes(makeTZNAME(2, self.tz[1].name))
if (
len(self.appointments)>0 or
len(self.todos)>0 or
len(self.phonenumbers)>0 or
len(self.anniversaries)>0
):
data += bytes(makeSTART2(1))
data += bytes(makeDATA1(self.appointments, self.todos, self.phonenumbers, self.anniversaries))
data += bytes(makeEND1())
# I don't know if all five alarms must be sent every time.
# If so, make dummy alarms.
if len(self.alarms)>0:
i=1
for a in self.alarms:
data += makeALARM(a, i)
i+=1
data += bytes(makeEND2())
return data
+2
View File
@@ -0,0 +1,2 @@
from .Blaster import *
from .TimexData import *
+189
View File
@@ -0,0 +1,189 @@
from crccheck.crc import CrcArc
# Create character conversion table (to be verified)
"""
char set for labels is: (6 bits per char)
0-9 : digits
10-36: letters
37-63: symbols:
space !"#$%&'()*+,-./;\
divide =
bell (image, not sound)
?
"""
char_conv = None
def make_char_conv():
global char_conv
# All lowercase characters. Using uppercase D for divide symbol and B for bell symbol
dst = "0123456789abcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./;\\D=B?"
src = range(len(dst))
char_conv = {k:v for k,v in zip(dst,src)}
# Convert string to timex string format using table from above
def str2timex(string):
if char_conv is None:
make_char_conv()
out = []
for c in string:
if not c in char_conv:
raise Exception("Invalid character {} in string!".format(c))
out.append(char_conv[c])
return out
# Pack 4 bytes in 3, used for strings
def pack4to3(indata):
while len(indata)%4:
# Add padding
indata.append(0xff)
outdata = []
while len(indata)>0:
ch1 = indata.pop(0)
ch2 = indata.pop(0)
ch3 = indata.pop(0)
ch4 = indata.pop(0)
outdata.append( ((ch2&0x03)<<6) | (ch1&0x3F) )
outdata.append( ((ch3&0x0F)<<4) | ((ch2>>2)&0x0F) )
outdata.append( ((ch4&0x3F)<<2) | ((ch3&0x30)>>4) )
return outdata
# Takes a list of bytes, returns list of bytes in package with length and checksum
def makepkg(values):
packet = []
packet.append(len(values)+3)
packet += values
p = CrcArc()
p.process(packet)
crc = p.final()
packet.append(crc>>8 & 0xFF)
packet.append(crc & 0xFF)
return packet
# Just dump list of ints in hex, for debugging convenience
def pkgstr(pkg):
outstr = ""
for b in pkg:
outstr += "0x{:02x} ".format(b)
return outstr[:-1]
### Packet makers
# Make start package. Currently only implemented for model 70.
def makeSTART1():
return makepkg([0x20, 0x00, 0x00, 0x01])
# num_data1: Number of data1 packets following
def makeSTART2(num_data1):
return makepkg([0x60, num_data1])
def makeEND1():
return makepkg([0x62])
def makeEND2():
return makepkg([0x21])
# Takes lists of TimexAppointment, TimexTodo, TimexPhoneNumber
# and TimexAnniversary objects
def makeDATA1(appts, todos, phones, anniversaries):
# TODO: For now, this assumes everything fits in one packet.
# If there is a lot of data, this may be split up over
# more than one packet.
data = [1] # Sequence ID
data += [0,0] # Start index for appointments, to be filled later
data += [0,0] # Start index for todos, to be filled later
data += [0,0] # Start index for phone numbers, to be filled later
data += [0,0] # Start index for anniversaries, to be filled later
data += [len(appts)]
data += [len(todos)]
data += [len(phones)]
data += [len(anniversaries)]
data += [0x14] # The github repo had 0x60 here, with a question mark
data += [0x03] # Time, in five minute intervals, to alarm before appointments
# Add appointments
index = len(data)-1
data[1] = (index&0xFF00) >> 8
data[2] = (index&0x00FF)
for a in appts:
data += list(bytes(a))
# Add todos
index = len(data)-1
data[3] = (index&0xFF00) >> 8
data[4] = (index&0x00FF)
for a in todos:
data += list(bytes(a))
# Add phone numbers
index = len(data)-1
data[5] = (index&0xFF00) >> 8
data[6] = (index&0x00FF)
for a in phones:
data += list(bytes(a))
# Add anniversaries
index = len(data)-1
data[7] = (index&0xFF00) >> 8
data[8] = (index&0x00FF)
for a in anniversaries:
data += list(bytes(a))
return makepkg([0x61]+data)
# Currently only implemented for model 70.
def makeTZ(tzno, tztime, format):
data = [tzno,
tztime.hour, tztime.minute,
tztime.month, tztime.day, tztime.year%100,
tztime.weekday(), tztime.second]
if format == 12:
data += [1]
else:
data += [2]
return makepkg([0x30]+data)
def makeTZNAME(tzno, tzname):
if len(tzname)>3:
raise Exception("Time zone name too long!")
elif len(tzname)<3:
tzname += " "*(3-len(tzname))
data = [tzno]
data+= str2timex(tzname)
return makepkg([0x31]+data)
# Takes an alarm number and a sequence ID (1-5)
# Currently only implemented for model 70.
def makeALARM(alarm, seq):
data = [seq]
data += [alarm.hour, alarm.minute, alarm.month, alarm.day]
label = alarm.label
if len(label)<8: # Min 8 chars, pad with space
label +=" "*(8-len(label))
label = label[:8] # Max 8 chars
data += str2timex(label) # NOT packed!
if alarm.audible:
data += [1]
else:
data += [0]
pkgdata = makepkg([0x50]+data)
# Add that packet that's present after inaudible alarms
if not alarm.audible:
data = [0, 0x61+seq, 0]
pkgdata += makepkg([0x70]+data)
return bytes(pkgdata)
+66
View File
@@ -0,0 +1,66 @@
import pytimex
def listhex(pkg):
outstr = ""
for b in pkg:
outstr += "0x{:02x}, ".format(b)
return outstr[:-1]
# Setup data to be sent
d = pytimex.TimexData()
if 0:
# Try all the features!
a = d.addNewAppointment(5, 31, 0x27, "test")
d.addNewTodo(3, "code stuff")
d.addNewPhoneNumber(73579, "e.t. home")
d.addNewAnniversary(6, 6, "national day")
# You can modify these later:
a.label = "hello"
# Setup a good timezone and a silly one just for testing
d.setTimezone(1, +2, 24, "cet")
d.setTimezone(2, -4.5, 24, "hej")
d.sendTime = True
# Add some alarms.
# Don't know if you need to send all five at once or if you can overwrite them individually.
d.addNewAlarm(9,0,0,0,"sample",True)
d.addNewAlarm(hour=10, minute=15, month=0, day=30, label="plug meeting", audible=True)
d.addNewAlarm(0, 0, 0, 0, "test", False)
d.addNewAlarm(0, 0, 0, 0, "alarm #4", False)
d.addNewAlarm(0, 0, 0, 0, "alarm #5", False)
else:
d.setTimezone(1, +2, 24, "cet")
d.setTimezone(2, -4.5, 24, "hej")
d.sendTime = True
# Get data to be transferred
data = bytes(d)
# Show data, for debugging
print("Data to be blasted:")
print(listhex(data))
sys.exit(0)
# Initialize blaster
b = pytimex.Blaster("/dev/ttyACM0", syncbuflen=16)
if not b.identify():
print("Could not verify adapter :(")
sys.exit(-1)
# Send synchronization byes (0x55 and 0xAA)
b.send_sync()
# Blast data
for databyte in data:
b.blast(databyte)
# Flush and wait for all bytes to sync
b.flush()
print("Done!")
+19
View File
@@ -0,0 +1,19 @@
import pytimex
def listhex(pkg):
outstr = ""
for b in pkg:
outstr += "0x{:02x}, ".format(b)
return outstr[:-1]
d = pytimex.TimexData()
#(self, hour=0, minute=0, month=0, day=0, label="", audible=True):
d.addNewAlarm(9,0,0,0,"sample",True)
d.addNewAlarm(1, 1, 5, 21, "cafe", True)
d.addNewAlarm(0, 0, 0, 23, "babee", True)
d.addNewAlarm(0, 0, 0, 0, "alarm #4", False)
d.addNewAlarm(0, 0, 0, 0, "alarm #5", False)
data = bytes(d)
print(listhex(data))
+81
View File
@@ -0,0 +1,81 @@
import serial
import time
import sys
"""
On reset the device can respond to 3 commands:
* 'x': Device detection, echo this so the PC knows you're there
* '?': Device ID query, reply with "M764" and a null byte ('\0')
* 'U': Reply with 'x' and enter transmit mode
In transmit mode, all bytes should be echoed back to the PC and the
commands above should not be answered. Transmit mode is only left upon
reset.
The device is powered from the CTS control line, so this is pulled down
at the beginning of transmission to make sure the device is reset.
"""
if len(sys.argv) != 3 and len(sys.argv) != 4:
print("Usage: {} <port> <log file> [bin|txt]".format(sys.argv[0]))
sys.exit(-1)
serialPort = sys.argv[1]
logFileName = sys.argv[2]
logText = False
if len(sys.argv) == 4:
if sys.argv[4] == txt:
logText = True
with serial.Serial(serialPort, 9600, timeout=0.1) as sp:
# Used to control state of device
transmitState = False
def send(data):
# print("Sending: '{}'".format(data))
sp.write(data)
while True:
while True:
# CTS control line is used for powering device, and therefore also reset it
if False and sp.cts == False:
if transmitState:
print("Leaving transmit state")
logfile.close()
transmitState = False
inb = sp.read(1)
if len(inb) != 0:
break
print("Received: {}".format(inb))
if not transmitState:
if inb == b"x":
print("Received detection byte ('x')")
send(inb)
elif inb == b"?":
print("Received device ID query ('?')")
send("M764\0".encode())
elif inb == b"U":
print("Received sync byte, entering transmit state")
transmitState = True
logfile = open(logFileName, 'wb')
send(inb)
else:
print("Received unknown byte ({}) outside of transmit state".format(inb))
else:
if logText:
logfile.write(("0x{:02x} \n".format(ord(inb))).encode())
else:
logfile.write(inb)
send(inb)