diff --git a/ipaacalib/python/src/ipaaca/__init__.py b/ipaacalib/python/src/ipaaca/__init__.py
index 41c3748d02464ff9ff774149b34aa8e30fe4273c..db2f873b9ca7f6405ba6ba4ba8a27b61f0a3f29d 100755
--- a/ipaacalib/python/src/ipaaca/__init__.py
+++ b/ipaacalib/python/src/ipaaca/__init__.py
@@ -1413,10 +1413,10 @@ class OutputBuffer(Buffer):
 		with iu.revision_lock:
 			if (update.revision != 0) and (update.revision != iu.revision):
 				# (0 means "do not pay attention to the revision number" -> "force update")
-				logger.warning("Remote update_payload operation failed because request was out of date; IU "+str(update.uid))
-				logger.warning("  Writer was: "+update.writer_name)
-				logger.warning("  Requested update was: (New keys:) "+','.join(update.new_items.keys())+'  (Removed keys:) '+','.join(update.keys_to_remove))
-				logger.warning("  Referred-to revision was "+str(update.revision)+' while local revision is '+str(iu.revision))
+				logger.warning(u"Remote update_payload operation failed because request was out of date; IU "+str(update.uid))
+				logger.warning(u"  Writer was: "+update.writer_name)
+				logger.warning(u"  Requested update was: (New keys:) "+','.join(update.new_items.keys())+'  (Removed keys:) '+','.join(update.keys_to_remove))
+				logger.warning(u"  Referred-to revision was "+str(update.revision)+' while local revision is '+str(iu.revision))
 				return 0
 			if update.is_delta:
 				#print('Writing delta update by '+str(update.writer_name))
diff --git a/ipaacalib/python/src/ipaaca/util/notifier.py b/ipaacalib/python/src/ipaaca/util/notifier.py
index 438ad760ed935c803fafde9a5fb6ddd427799006..1ee991e833af8a776b8c040ac8fc2d68e687b7b0 100644
--- a/ipaacalib/python/src/ipaaca/util/notifier.py
+++ b/ipaacalib/python/src/ipaaca/util/notifier.py
@@ -35,6 +35,7 @@ from __future__ import print_function, with_statement
 import threading
 
 import ipaaca
+from ipaaca.util.timesync import *
 
 NotificationState = ipaaca.enum(
 		NEW = 'new',
@@ -68,6 +69,11 @@ class ComponentNotifier(object):
 		self.initialize_lock = threading.Lock()
 		self.notification_handler_lock = threading.Lock()
 		self.submit_lock = threading.Lock()
+		# clock sync code, sync slave/master pair will be installed when launched
+		self.timesync_slave = None
+		self.timesync_master = None
+		self.timesync_master_handlers = []
+		self.timesync_slave_handlers = []
 
 	def _submit_notify(self, is_new):
 		with self.submit_lock:
@@ -109,11 +115,32 @@ class ComponentNotifier(object):
 		with self.notification_handler_lock:
 			self.notification_handlers.append(handler)
 
+	def launch_timesync_slave_handlers(self, master, slave, latency, offset):
+		for h in self.timesync_slave_handlers:
+			h(master, slave, latency, offset)
+
+	def launch_timesync_master_handlers(self, master, slave, latency, offset):
+		for h in self.timesync_master_handlers:
+			h(master, slave, latency, offset)
+
+	def add_timesync_slave_handler(self, handler):
+		self.timesync_slave_handlers.append(handler)
+	
+	def add_timesync_master_handler(self, handler):
+		self.timesync_master_handlers.append(handler)
+	
+	def send_master_timesync(self):
+		#if len(self.timesync_master_handlers)==0:
+		#	print('Warning: Sending a master timesync without a registered result callback.')
+		self.timesync_master.send_master_timesync()
+	
 	def initialize(self):
 		with self.initialize_lock:
 			if self.terminated:
 				raise ComponentError('Attempted to reinitialize component '+component_name+' after termination')
 			if (not self.initialized):
+				self.timesync_slave = TimesyncSlave(component_name=self.component_name, timing_handler=self.launch_timesync_slave_handlers)
+				self.timesync_master = TimesyncMaster(component_name=self.component_name, timing_handler=self.launch_timesync_master_handlers)
 				self.in_buffer.register_handler(self._handle_iu_event, ipaaca.IUEventType.MESSAGE, ComponentNotifier.NOTIFY_CATEGORY)
 				self._submit_notify(True)
 				self.initialized = True
diff --git a/ipaacalib/python/src/ipaaca/util/timesync.py b/ipaacalib/python/src/ipaaca/util/timesync.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3ec33e58398d37eb2b714c8625bbd3af22d1107
--- /dev/null
+++ b/ipaacalib/python/src/ipaaca/util/timesync.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import ipaaca
+import time
+
+class TimesyncMaster(object):
+	def __init__(self, component_name=None, timing_handler=None, debug_offset=0):
+		self.ob = ipaaca.OutputBuffer(('' if component_name is None else component_name)+'TimesyncMaster')
+		self.ib = ipaaca.InputBuffer(('' if component_name is None else component_name)+'TimesyncMaster', ['timesyncReply'])
+		# component name to report (None => use buffer name)
+		self.component_name = component_name if component_name is not None else self.ob.unique_name
+		#
+		self.ob.register_handler(self.handle_timesync_master)
+		self.ib.register_handler(self.handle_timesync_master)
+		# master_t1 is identical for all slaves
+		self.master_t1 = None
+		self.slave_t1s = {}
+		self.master_t2s = {}
+		self.slave_t2s = {}
+		self.latencies = {}
+		self.time_offsets = {}
+		#
+		self.debug_offset = debug_offset
+		#
+		self.timing_handler = timing_handler
+	
+	def set_timing_handler(self, timing_handler):
+		self.timing_handler = timing_handler
+	
+	def send_master_timesync(self):
+		iu = ipaaca.IU('timesyncRequest')
+		self.master_t1 = self.get_time()
+		iu.payload = {
+				'stage':'0',
+				'master_t1':str(self.master_t1),
+				'master':self.component_name,
+				}
+		self.ob.add(iu)
+		print('Sent a stage 0 timesync as master '+self.component_name)
+	
+	def handle_timesync_master(self, iu, event_type, own):
+		master = iu.payload['master']
+		if not own and master == self.component_name:
+			if self.component_name == master:
+				# reply to our own initial IU
+				slave = iu.payload['slave']
+				stage = iu.payload['stage']
+				if stage=='1':
+					print('Received stage 1 from slave '+slave)
+					# initial reply by slave
+					t1 = iu.payload['slave_t1']
+					self.slave_t1s[slave] = float(t1)
+					t2 = self.master_t2s[slave] = self.get_time()
+					iu.payload.merge({'master_t2': str(t2), 'stage':'2'})
+					latency1 = t2 - self.master_t1
+					self.latencies[slave] = latency1
+					#print('Latency of round-trip 1: %.3f' % latency1)
+				elif stage=='3':
+					print('Received stage 3 from slave '+slave)
+					# second reply by slave
+					t2 = iu.payload['slave_t2']
+					self.slave_t2s[slave] = float(t2)
+					t_final = self.get_time()
+					latency1 = self.latencies[slave]
+					latency2 = t_final - self.master_t2s[slave]
+					latency = self.latencies[slave] = (latency1+latency2)/2.0
+					offset1 = (self.slave_t1s[slave]-self.master_t1)-latency/2.0
+					offset2 = (self.slave_t2s[slave]-self.master_t2s[slave])-latency/2.0
+					offset = (offset1+offset2)/2.0
+					iu.payload.merge({'stage':'4', 'latency': str(latency), 'offset':str(offset)})
+					if self.timing_handler is None:
+						print('Determined timing of timesync slave '+slave)
+						print('  Avg round-trip latency: %.3f s'%latency)
+						print('  Offset of their clock: %.3f s'%offset)
+					else:
+						self.timing_handler(self.component_name, slave, latency, offset)
+				else:
+					# other stages are handled by time slave handler
+					pass
+	
+	def get_time(self):
+		return time.time() + self.debug_offset
+
+
+class TimesyncSlave(object):
+	def __init__(self, component_name=None, timing_handler=None, debug_offset=0):
+		self.ob = ipaaca.OutputBuffer(('' if component_name is None else component_name)+'TimesyncSlave')
+		self.ib = ipaaca.InputBuffer(('' if component_name is None else component_name)+'TimesyncSlave', ['timesyncRequest'])
+		# component name to report (None => use buffer name)
+		self.component_name = component_name if component_name is not None else self.ib.unique_name
+		self.ob.register_handler(self.handle_timesync_slave)
+		self.ib.register_handler(self.handle_timesync_slave)
+		#self.master_t1 = None
+		#self.master_t2 = None
+		#self.master = None
+		self.latency = None
+		self.my_iu = None
+		#
+		self.debug_offset = debug_offset
+		#
+		self.timing_handler = timing_handler
+	
+	def set_timing_handler(self, timing_handler):
+		self.timing_handler = timing_handler
+	
+	def handle_timesync_slave(self, iu, event_type, own):
+		master = iu.payload['master']
+		stage = iu.payload['stage']
+		if self.component_name != master:
+			if not own:
+				# reply only to IUs from others
+				if stage=='0':
+					#print('Received stage 0 from master '+master)
+					# initial reply to master
+					self.my_iu = ipaaca.IU('timesyncReply')
+					# TODO: add grounded_in link too?
+					t1 = self.get_time()
+					self.my_iu.payload = iu.payload
+					self.my_iu.payload['slave'] = self.component_name
+					self.my_iu.payload['slave_t1'] = str(t1)
+					self.my_iu.payload['stage'] = '1'
+
+					#self.my_iu.payload.merge({
+					#		'slave':self.component_name,
+					#		'slave_t1':str(t1),
+					#		'stage':'1',
+					#		})
+					self.ob.add(self.my_iu)
+			else:
+				if stage=='2':
+					#print('Received stage 2 from master '+master)
+					t2 = self.get_time()
+					self.my_iu.payload.merge({
+							'slave_t2':str(t2),
+							'stage':'3',
+							})
+				elif stage=='4':
+					latency = float(iu.payload['latency'])
+					offset = float(iu.payload['offset'])
+					if self.timing_handler is None:
+						print('Timesync master '+master+' determined our timing: ')
+						print('  Avg round-trip latency: %.3f s'%latency)
+						print('  Offset of our clock: %.3f s'%offset)
+					else:
+						self.timing_handler(master, self.component_name, latency, offset)
+	def get_time(self):
+		return time.time() + self.debug_offset
+
+