1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.hdmicec.cts; 18 19 import com.android.tradefed.log.LogUtil.CLog; 20 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 21 import com.android.tradefed.util.RunUtil; 22 23 import java.io.BufferedReader; 24 import java.io.BufferedWriter; 25 import java.io.InputStreamReader; 26 import java.io.OutputStreamWriter; 27 import java.util.concurrent.TimeUnit; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.List; 31 import java.util.regex.Pattern; 32 33 import org.junit.rules.ExternalResource; 34 35 /** Class that helps communicate with the cec-client */ 36 public final class HdmiCecClientWrapper extends ExternalResource { 37 38 private static final String CEC_CONSOLE_READY = "waiting for input"; 39 private static final int MILLISECONDS_TO_READY = 10000; 40 private static final int DEFAULT_TIMEOUT = 20000; 41 private static final int BUFFER_SIZE = 1024; 42 43 private Process mCecClient; 44 private BufferedWriter mOutputConsole; 45 private BufferedReader mInputConsole; 46 private boolean mCecClientInitialised = false; 47 48 private LogicalAddress targetDevice; 49 private String clientParams[]; 50 HdmiCecClientWrapper(LogicalAddress targetDevice, String ...clientParams)51 public HdmiCecClientWrapper(LogicalAddress targetDevice, String ...clientParams) { 52 this.targetDevice = targetDevice; 53 this.clientParams = clientParams; 54 } 55 56 @Override before()57 protected void before() throws Throwable { 58 this.init(); 59 }; 60 61 @Override after()62 protected void after() { 63 this.killCecProcess(); 64 }; 65 66 /** Initialise the client */ init()67 private void init() throws Exception { 68 List<String> commands = new ArrayList(); 69 70 commands.add("cec-client"); 71 /* "-p 2" starts the client as if it is connected to HDMI port 2, taking the physical 72 * address 2.0.0.0 */ 73 commands.add("-p"); 74 commands.add("2"); 75 /* "-t x" starts the client as a TV device */ 76 commands.add("-t"); 77 commands.add("x"); 78 commands.addAll(Arrays.asList(clientParams)); 79 80 mCecClient = RunUtil.getDefault().runCmdInBackground(commands); 81 mInputConsole = new BufferedReader(new InputStreamReader(mCecClient.getInputStream())); 82 83 /* Wait for the client to become ready */ 84 mCecClientInitialised = true; 85 if (checkConsoleOutput(CecClientMessage.CLIENT_CONSOLE_READY + "", MILLISECONDS_TO_READY)) { 86 mOutputConsole = new BufferedWriter( 87 new OutputStreamWriter(mCecClient.getOutputStream()), BUFFER_SIZE); 88 return; 89 } 90 91 mCecClientInitialised = false; 92 93 throw (new Exception("Could not initialise cec-client process")); 94 } 95 checkCecClient()96 private void checkCecClient() throws Exception { 97 if (!mCecClientInitialised) { 98 throw new Exception("cec-client not initialised!"); 99 } 100 if (!mCecClient.isAlive()) { 101 throw new Exception("cec-client not running!"); 102 } 103 } 104 105 /** 106 * Sends a CEC message with source marked as broadcast to the device passed in the constructor 107 * through the output console of the cec-communication channel. 108 */ sendCecMessage(CecOperand message)109 public void sendCecMessage(CecOperand message) throws Exception { 110 sendCecMessage(LogicalAddress.BROADCAST, targetDevice, message, ""); 111 } 112 113 /** 114 * Sends a CEC message from source device to the device passed in the constructor through the 115 * output console of the cec-communication channel. 116 */ sendCecMessage(LogicalAddress source, CecOperand message)117 public void sendCecMessage(LogicalAddress source, CecOperand message) throws Exception { 118 sendCecMessage(source, targetDevice, message, ""); 119 } 120 121 /** 122 * Sends a CEC message from source device to a destination device through the output console of 123 * the cec-communication channel. 124 */ sendCecMessage(LogicalAddress source, LogicalAddress destination, CecOperand message)125 public void sendCecMessage(LogicalAddress source, LogicalAddress destination, 126 CecOperand message) throws Exception { 127 sendCecMessage(source, destination, message, ""); 128 } 129 130 /** 131 * Sends a CEC message from source device to a destination device through the output console of 132 * the cec-communication channel with the appended params. 133 */ sendCecMessage(LogicalAddress source, LogicalAddress destination, CecOperand message, String params)134 public void sendCecMessage(LogicalAddress source, LogicalAddress destination, 135 CecOperand message, String params) throws Exception { 136 checkCecClient(); 137 mOutputConsole.write("tx " + source + destination + ":" + message + params); 138 mOutputConsole.newLine(); 139 mOutputConsole.flush(); 140 } 141 142 /** 143 * Sends a <USER_CONTROL_PRESSED> and <USER_CONTROL_RELEASED> from source to destination 144 * through the output console of the cec-communication channel with the mentioned keycode. 145 */ sendUserControlPressAndRelease(LogicalAddress source, LogicalAddress destination, int keycode, boolean holdKey)146 public void sendUserControlPressAndRelease(LogicalAddress source, LogicalAddress destination, 147 int keycode, boolean holdKey) throws Exception { 148 sendUserControlPress(source, destination, keycode, holdKey); 149 /* Sleep less than 200ms between press and release */ 150 TimeUnit.MILLISECONDS.sleep(100); 151 mOutputConsole.write("tx " + source + destination + ":" + 152 CecOperand.USER_CONTROL_RELEASED); 153 mOutputConsole.flush(); 154 } 155 156 /** 157 * Sends a <UCP> message from source to destination through the output console of the 158 * cec-communication channel with the mentioned keycode. If holdKey is true, the method will 159 * send multiple <UCP> messages to simulate a long press. No <UCR> will be sent. 160 */ sendUserControlPress(LogicalAddress source, LogicalAddress destination, int keycode, boolean holdKey)161 public void sendUserControlPress(LogicalAddress source, LogicalAddress destination, 162 int keycode, boolean holdKey) throws Exception { 163 String key = String.format("%02x", keycode); 164 String command = "tx " + source + destination + ":" + 165 CecOperand.USER_CONTROL_PRESSED + ":" + key; 166 167 if (holdKey) { 168 /* Repeat once between 200ms and 450ms for at least 5 seconds. Since message will be 169 * sent once later, send 16 times in loop every 300ms. */ 170 int repeat = 16; 171 for (int i = 0; i < repeat; i++) { 172 mOutputConsole.write(command); 173 mOutputConsole.newLine(); 174 mOutputConsole.flush(); 175 TimeUnit.MILLISECONDS.sleep(300); 176 } 177 } 178 179 mOutputConsole.write(command); 180 mOutputConsole.newLine(); 181 mOutputConsole.flush(); 182 } 183 184 /** 185 * Sends a series of <UCP> [firstKeycode] from source to destination through the output console 186 * of the cec-communication channel immediately followed by <UCP> [secondKeycode]. No <UCR> 187 * message is sent. 188 */ sendUserControlInterruptedPressAndHold( LogicalAddress source, LogicalAddress destination, int firstKeycode, int secondKeycode, boolean holdKey)189 public void sendUserControlInterruptedPressAndHold( 190 LogicalAddress source, LogicalAddress destination, 191 int firstKeycode, int secondKeycode, boolean holdKey) throws Exception { 192 sendUserControlPress(source, destination, firstKeycode, holdKey); 193 /* Sleep less than 200ms between press and release */ 194 TimeUnit.MILLISECONDS.sleep(100); 195 sendUserControlPress(source, destination, secondKeycode, false); 196 } 197 198 /** Sends a message to the output console of the cec-client */ sendConsoleMessage(String message)199 public void sendConsoleMessage(String message) throws Exception { 200 checkCecClient(); 201 CLog.v("Sending message:: " + message); 202 mOutputConsole.write(message); 203 mOutputConsole.flush(); 204 } 205 206 /** Check for any string on the input console of the cec-client, uses default timeout */ checkConsoleOutput(String expectedMessage)207 public boolean checkConsoleOutput(String expectedMessage) throws Exception { 208 return checkConsoleOutput(expectedMessage, DEFAULT_TIMEOUT); 209 } 210 211 /** Check for any string on the input console of the cec-client */ checkConsoleOutput(String expectedMessage, long timeoutMillis)212 public boolean checkConsoleOutput(String expectedMessage, 213 long timeoutMillis) throws Exception { 214 checkCecClient(); 215 long startTime = System.currentTimeMillis(); 216 long endTime = startTime; 217 218 while ((endTime - startTime <= timeoutMillis)) { 219 if (mInputConsole.ready()) { 220 String line = mInputConsole.readLine(); 221 if (line.contains(expectedMessage)) { 222 CLog.v("Found " + expectedMessage + " in " + line); 223 return true; 224 } 225 } 226 endTime = System.currentTimeMillis(); 227 } 228 return false; 229 } 230 231 /** Gets all the messages received from the given source device during a period of duration 232 * seconds. 233 */ getAllMessages(LogicalAddress source, int duration)234 public List<CecOperand> getAllMessages(LogicalAddress source, int duration) throws Exception { 235 List<CecOperand> receivedOperands = new ArrayList<>(); 236 long startTime = System.currentTimeMillis(); 237 long endTime = startTime; 238 Pattern pattern = Pattern.compile("(.*>>)(.*?)" + 239 "(" + source + "\\p{XDigit}):(.*)", 240 Pattern.CASE_INSENSITIVE); 241 242 while ((endTime - startTime <= duration)) { 243 if (mInputConsole.ready()) { 244 String line = mInputConsole.readLine(); 245 if (pattern.matcher(line).matches()) { 246 CecOperand operand = CecMessage.getOperand(line); 247 if (!receivedOperands.contains(operand)) { 248 receivedOperands.add(operand); 249 } 250 } 251 } 252 endTime = System.currentTimeMillis(); 253 } 254 return receivedOperands; 255 } 256 257 258 /** 259 * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and 260 * returns the first line that contains that message within default timeout. If the CEC message 261 * is not found within the timeout, an exception is thrown. 262 */ checkExpectedOutput(CecOperand expectedMessage)263 public String checkExpectedOutput(CecOperand expectedMessage) throws Exception { 264 return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, DEFAULT_TIMEOUT); 265 } 266 267 /** 268 * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client 269 * communication channel and returns the first line that contains that message within 270 * default timeout. If the CEC message is not found within the timeout, an exception is thrown. 271 */ checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage)272 public String checkExpectedOutput(LogicalAddress toDevice, 273 CecOperand expectedMessage) throws Exception { 274 return checkExpectedOutput(toDevice, expectedMessage, DEFAULT_TIMEOUT); 275 } 276 277 /** 278 * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and 279 * returns the first line that contains that message within timeoutMillis. If the CEC message 280 * is not found within the timeout, an exception is thrown. 281 */ checkExpectedOutput(CecOperand expectedMessage, long timeoutMillis)282 public String checkExpectedOutput(CecOperand expectedMessage, 283 long timeoutMillis) throws Exception { 284 return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, timeoutMillis); 285 } 286 287 /** 288 * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client 289 * communication channel and returns the first line that contains that message within 290 * timeoutMillis. If the CEC message is not found within the timeout, an exception is thrown. 291 */ checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage, long timeoutMillis)292 public String checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage, 293 long timeoutMillis) throws Exception { 294 checkCecClient(); 295 long startTime = System.currentTimeMillis(); 296 long endTime = startTime; 297 Pattern pattern = Pattern.compile("(.*>>)(.*?)" + 298 "(" + targetDevice + toDevice + "):" + 299 "(" + expectedMessage + ")(.*)", 300 Pattern.CASE_INSENSITIVE); 301 302 while ((endTime - startTime <= timeoutMillis)) { 303 if (mInputConsole.ready()) { 304 String line = mInputConsole.readLine(); 305 if (pattern.matcher(line).matches()) { 306 CLog.v("Found " + expectedMessage.name() + " in " + line); 307 return line; 308 } 309 } 310 endTime = System.currentTimeMillis(); 311 } 312 throw new Exception("Could not find message " + expectedMessage.name()); 313 } 314 315 /** 316 * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client 317 * communication channel and throws an exception if it finds the line that contains the message 318 * within the default timeout. If the CEC message is not found within the timeout, function 319 * returns without error. 320 */ checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage)321 public void checkOutputDoesNotContainMessage(LogicalAddress toDevice, 322 CecOperand incorrectMessage) throws Exception { 323 checkOutputDoesNotContainMessage(toDevice, incorrectMessage, DEFAULT_TIMEOUT); 324 } 325 326 /** 327 * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client 328 * communication channel and throws an exception if it finds the line that contains the message 329 * within timeoutMillis. If the CEC message is not found within the timeout, function returns 330 * without error. 331 */ checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage, long timeoutMillis)332 public void checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage, 333 long timeoutMillis) throws Exception { 334 335 checkCecClient(); 336 long startTime = System.currentTimeMillis(); 337 long endTime = startTime; 338 Pattern pattern = Pattern.compile("(.*>>)(.*?)" + 339 "(" + targetDevice + toDevice + "):" + 340 "(" + incorrectMessage + ")(.*)", 341 Pattern.CASE_INSENSITIVE); 342 343 while ((endTime - startTime <= timeoutMillis)) { 344 if (mInputConsole.ready()) { 345 String line = mInputConsole.readLine(); 346 if (pattern.matcher(line).matches()) { 347 CLog.v("Found " + incorrectMessage.name() + " in " + line); 348 throw new Exception("Found " + incorrectMessage.name() + " to " + toDevice + 349 " with params " + CecMessage.getParamsAsString(line)); 350 } 351 } 352 endTime = System.currentTimeMillis(); 353 } 354 } 355 356 /** 357 * Kills the cec-client process that was created in init(). 358 */ killCecProcess()359 private void killCecProcess() { 360 try { 361 checkCecClient(); 362 sendConsoleMessage(CecClientMessage.QUIT_CLIENT.toString()); 363 mOutputConsole.close(); 364 mInputConsole.close(); 365 mCecClientInitialised = false; 366 if (!mCecClient.waitFor(MILLISECONDS_TO_READY, TimeUnit.MILLISECONDS)) { 367 /* Use a pkill cec-client if the cec-client process is not dead in spite of the 368 * quit above. 369 */ 370 List<String> commands = new ArrayList<>(); 371 Process killProcess; 372 commands.add("pkill"); 373 commands.add("cec-client"); 374 killProcess = RunUtil.getDefault().runCmdInBackground(commands); 375 killProcess.waitFor(); 376 } 377 } catch (Exception e) { 378 /* If cec-client is not running, do not throw an exception, just return. */ 379 CLog.w("Unable to close cec-client", e); 380 } 381 } 382 } 383