1#!/usr/bin/env python3 2# 3# Copyright (C) 2023 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import argparse 18from datetime import datetime 19from threading import Thread 20import os 21import re 22import sys 23import time 24import traceback 25 26# May import this package in the workstation with: 27# pip install paramiko 28from paramiko import SSHClient 29from paramiko import AutoAddPolicy 30 31from prepare_tracing import adb_run 32 33# Usage: 34# ./calculate_time_offset.py --host_username root --host_ip 10.42.0.247 35# --guest_serial 10.42.0.247 --clock_name CLOCK_REALTIME 36# or 37# ./calculate_time_offset.py --host_username root --host_ip 10.42.0.247 38# --guest_serial 10.42.0.247 --clock_name CLOCK_REALTIME --mode trace 39 40class Device: 41 # Get the machine time 42 def __init__(self, clock_name, mode): 43 if clock_name != None: 44 self.time_cmd += f' {clock_name}' 45 if mode == "trace": 46 if clock_name == None: 47 raise SystemExit("Error: with trace mode, clock_name must be specified") 48 self.time_cmd = f'{self.time_cmd} --trace' 49 def GetTime(self): 50 pass 51 52 def ParseTime(self, time_str): 53 pattern = r'\d+' 54 match = re.search(pattern, time_str) 55 if match is None: 56 raise Exception(f'Error: ParseTime no match time string: {time_str}') 57 return int(match.group()) 58 59 # Here is an example of time_util with --trace flag enable and given a clockname 60 # will give a snapshot of the CPU counter and clock timestamp. 61 # time_util CLOCK_REALTIME --trace 62 # 6750504532818 CPU tick value 63 # 1686355159395639260 CLOCK_REATIME 64 # 0.0192 CPU tick per nanosecond 65 # 66 # The example's output is ts_str 67 def TraceTime(self, ts_str): 68 lines = ts_str.split("\n") 69 if len(lines) < 3: 70 raise Exception(f'Error: TraceTime input is wrong {ts_str}.' 71 'Expecting three lines of input: ' 72 'cpu_tick_value, CLOCK value, and CPU cycles per nanoseconds') 73 74 self.cpu_ts = int(lines[0]) 75 self.clock_ts = int(lines[1]) 76 self.cpu_cycles = float(lines[2]) 77 78class QnxDevice(Device): 79 def __init__(self, host_username, host_ip, clock_name, mode): 80 self.sshclient = SSHClient() 81 self.sshclient.load_system_host_keys() 82 self.sshclient.set_missing_host_key_policy(AutoAddPolicy()) 83 self.sshclient.connect(host_ip, username=host_username) 84 self.time_cmd = "/bin/QnxClocktime" 85 super().__init__(clock_name, mode) 86 87 def GetTime(self): 88 (stdin, stdout, stderr) = self.sshclient.exec_command(self.time_cmd) 89 return stdout 90 91 def ParseTime(self, time_str): 92 time_decoded_str = time_str.read().decode() 93 return super().ParseTime(time_decoded_str) 94 95 def TraceTime(self): 96 result_str = self.GetTime() 97 ts_str = result_str.read().decode() 98 super().TraceTime(ts_str) 99 100class AndroidDevice(Device): 101 def __init__(self, guest_serial, clock_name, mode): 102 adb_run(guest_serial, ['connect']) 103 self.time_cmd = "/vendor/bin/android.automotive.time_util" 104 self.serial = guest_serial 105 super().__init__(clock_name, mode) 106 107 def GetTime(self): 108 ts = adb_run(self.serial, ['shell', self.time_cmd]) 109 return ts 110 111 def TraceTime(self): 112 super().TraceTime(self.GetTime()) 113 114# measure the time offset between device1 and device2 with ptp, 115# return the average value over cnt times. 116def Ptp(device1, device2): 117 # set up max delay as 100 milliseconds 118 max_delay_ms = 100000000 119 # set up max offset as 2 milliseconds 120 max_offset_ms = 2000000 121 max_retry = 20 122 for i in range(max_retry): 123 time1_d1_str = device1.GetTime() 124 time1_d2_str = device2.GetTime() 125 time2_d2_str = device2.GetTime() 126 time2_d1_str = device1.GetTime() 127 128 time1_d1 = device1.ParseTime(time1_d1_str) 129 time2_d1 = device1.ParseTime(time2_d1_str) 130 time1_d2 = device2.ParseTime(time1_d2_str) 131 time2_d2 = device2.ParseTime(time2_d2_str) 132 133 offset = (time1_d2 + time2_d2 - time1_d1 - time2_d1)/2 134 if time2_d1 - time1_d1 > max_delay_ms or time2_d2 - time2_d2 > max_delay_ms or abs(offset) > max_offset_ms: 135 print(f'Network delay is too big, ignore this measure {offset}') 136 else: 137 return int(offset) 138 raise SystemExit(f"Network delay is still too big after {max_retry} retries") 139 140# It assumes device1 and device2 have access to the same CPU counter and uses the cpu counter 141# as the time source to calculate the time offset between device1 and device2. 142def TraceTimeOffset(device1, device2): 143 offset = device2.clock_ts - device1.clock_ts - ((device2.cpu_ts - device1.cpu_ts)/device2.cpu_cycles) 144 return int(offset) 145 146def CalculateTimeOffset(host_username, hostip, guest_serial, clock_name, mode): 147 qnx = QnxDevice(host_username, hostip, clock_name, mode) 148 android = AndroidDevice(guest_serial, clock_name, mode) 149 if mode == "trace": 150 return TraceTimeOffset(qnx, android) 151 else: 152 return Ptp(qnx, android) 153 154 155def ParseArguments(): 156 parser = argparse.ArgumentParser() 157 parser.add_argument('--host_ip', required=True, 158 help = 'host IP address') 159 parser.add_argument('--host_username', required=True, 160 help = 'host username') 161 parser.add_argument('--guest_serial', required=True, 162 help = 'guest VM serial number') 163 parser.add_argument('--clock_name', required=False, choices =['CLOCK_REALTIME','CLOCK_MONOTONIC'], 164 help = 'clock that will be used for the measument. By default CPU counter is used.') 165 parser.add_argument('--mode', choices=['ptp', 'trace'], default='ptp', 166 help='select the mode of operation. If the two devices have access of the same CPU counter, ' 167 'use trace option. Otherwise use ptp option.') 168 return parser.parse_args() 169 170def main(): 171 args = ParseArguments() 172 time_offset = CalculateTimeOffset(args.host_username, args.host_ip, args.guest_serial, args.clock_name, args.mode) 173 print(f'Time offset between host and guest is {time_offset} nanoseconds') 174if __name__ == "__main__": 175 main() 176