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