├── .gitignore ├── CHANGELOG_1.x_to_2.0 ├── CONTRIBUTING ├── LICENSE ├── README ├── _common.py ├── _config.py ├── _cpslib.py ├── _device.py ├── _gdocs.py ├── _log.py ├── _oauth2.py ├── _privet.py ├── _sheets.py ├── _ticket.py ├── _transport.py ├── _zconf.py ├── console_output.png ├── images ├── 6MB.gif ├── A4testpage.png ├── Chartley_Castle-1.jpg ├── ChromeOSPowerManagementSpec.html ├── DoNotDisturb.svg ├── Example.svg ├── Google-Glass.gif ├── GoogleArt.jpg ├── GoogleCampus.jpg ├── GoogleGlass.jpg ├── GoogleGlass2.jpg ├── PDF1.2.pdf ├── PDF1.3.pdf ├── PDF1.4.pdf ├── PDF1.5.pdf ├── PDF1.6.pdf ├── PDF1.7.pdf ├── Satake_AE_web.pdf ├── YourTickets.pdf ├── a3color.pdf ├── b&w-test.jpg ├── boardingpass.pdf ├── brin.jpg ├── colorkey.jpg ├── dna_overview.png ├── fileinjpeg.jpeg ├── gcpbeta.png ├── gcpreglink.tif ├── google-car.jpg ├── google_logo.png ├── img_0012.gif ├── landscape-test.jpg ├── largeref.jpg ├── larrypage.png ├── letter_p.pdf ├── lorem.pdf ├── malformatted.pdf ├── mandlebulb_3d_test.png ├── marbles.tif ├── margin-test.pdf ├── max_test_big.jpg ├── multitarget5.jpg ├── noise.pdf ├── pickrpt.pdf ├── poster.gif ├── printtest.pdf ├── printtest.png ├── printtest2.png ├── rosemary.pdf ├── stepchart.jpg ├── testpage.png ├── testprint.jpeg ├── ticket.pdf └── version4pdf.pdf ├── testcert.py └── version /.gitignore: -------------------------------------------------------------------------------- 1 | Features and unchecking 'Suppress Bells' 36 | 37 | - Google sheet output is better formatted for readability. On test failures, 38 | a single testcase re-run command is added so you can focus on passing that 39 | test and without having to re-run the other tests in the suite 40 | 41 | - The old master branch is now branched into 'legacy_master' on GitHub -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Want to contribute? 2 | Great! First, read this page (including the small print at the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement]: 7 | (https://cla.developers.google.com/about/google-individual) 8 | (CLA), which you can do online. The CLA is necessary mainly because you own the 9 | copyright to your changes, even after your contribution becomes part of our 10 | codebase, so we need your permission to use and distribute your code. We also 11 | need to be sure of various other things—for instance that you'll tell us if you 12 | know that your code infringes on other people's patents. You don't have to sign 13 | the CLA until after you've submitted your code for review and a member has 14 | approved it, but you must do it before we can put your code into our codebase. 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. We 22 | use Github pull requests for this purpose. 23 | 24 | ### The small print 25 | Contributions made by corporations are covered by a different agreement than 26 | the one above, the 27 | [Software Grant and Corporate Contributor License Agreement] 28 | (https://cla.developers.google.com/about/google-corporate). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Google Cloud Print Logo Certification Tool 2 | 3 | The Logo Certification tool is a group of tests designed to aid in running 4 | Logo Certification tests that printers must pass in order to obtain Logo 5 | Certification. The tool is made up of a number of python files, and utilizes 6 | the python UnitTest module to actually execute the tests. 7 | 8 | List of modules and their purpose: 9 | 10 | --> testcert.py - the entry point and driver of the tests. All of the actual 11 | tests are located in this file. 12 | --> _common.py - some shared functions within the package. 13 | --> _config.py - configuration information. This needs to be updated for each 14 | specific device under test. This file needs to be edited by the user executing 15 | the test 16 | --> _ticket.py - Provides the Cloud Job Ticket (CJT) object and methods to 17 | specify various print options for both cloud printing and local printing 18 | --> _cpslib.py - Methods to access GCP API's 19 | --> _device.py - Methods to support populating the device attributes. 20 | --> _gdocs.py - Methods to interact with Google Docs and Google Drive. 21 | --> _jsonparser.py - Methods to parse and handle JSON formatted docs and strings. 22 | --> _log.py - Provides a logger to ensure proper logging of all activities. 23 | --> _zconf.py - Provides support for monitoring mdns advertisements. 24 | --> _oauth2.py - Provides support to get oauth2 tokens. 25 | --> _privet.py - Provides privet structures. 26 | --> _sheets.py - Uses _gdocs.py to create and populate a Google spreadsheet. 27 | --> _transport.py - Provides HTTP support for accessing web services. 28 | 29 | The tests are divided into suites the focus on specific areas. The areas tested 30 | are: 31 | 32 | 1. Privet Protocol Integration 33 | 2. Pre-registration Tests 34 | 3. Cloud Print Registration 35 | 4. Post-registration Tests 36 | 5. Local Discovery Tests 37 | 6. Local Print Tests 38 | 7. Printing Tests 39 | 8. Printer Capabilities 40 | 9. Printer State Tests 41 | 10. Job State Tests 42 | 11. Cloud Print Unregistration 43 | 12. Post-unregistration Tests 44 | 45 | DEPENDENCIES: 46 | (Instructions below are for Unix devices. For Windows, 47 | use easy_install.py from PythonDir/Scripts) 48 | 49 | Requests module is required for HTTP comms. To install, use the command: 50 | sudo pip install --upgrade requests 51 | 52 | OAuth2Client and Google API client are required, To install, use the commands: 53 | 54 | sudo pip install --upgrade oauth2client 55 | sudo pip install --upgrade google-api-python-client 56 | 57 | The Python Zeroconf package is used to execute some of the mDNS tests. Install 58 | ZeroConf from the package located here: 59 | 60 | Using PIP: 61 | sudo pip install --upgrade zeroconf 62 | 63 | Or download from one of the following: 64 | https://pypi.python.org/pypi/zeroconf 65 | https://github.com/jstasiak/python-zeroconf 66 | 67 | The tool will also store test results into a Google Spreadsheet. If you want 68 | to use this functionality, you will need to install gdata. Install gdata from: 69 | 70 | https://github.com/google/gdata-python-client 71 | 72 | gdata depends on tlslite, if you get an error stack trace such as: 73 | File "/usr/local/lib/python2.7/dist-packages/gdata/oauth/rsa.py", line 10 74 | from tlslite.utils import keyfactory 75 | ImportError: No module named tlslite.utils 76 | 77 | then install tlslite by running: 78 | sudo pip install tlslite 79 | 80 | gdata also depends on ElementTree; however, most systems will have ElementTree 81 | installed already. To test if it's installed, from a Python shell try: 82 | 83 | from xml.etree import ElementTree 84 | 85 | Before executing the Logo Certification Tool, a number of preparatory steps are 86 | required to get your test environment set up correctly. 87 | 88 | 1. Test Account - Use a test account that uses Gmail, in order to properly 89 | authenticate against Google properties and also use OAuth2 credentials. Once 90 | you have obtained a test account, obtain OAuth2 credentials from the Google 91 | Developers Console. You'll also need a 2nd test account, without the OAuth2 92 | credentials. 93 | 94 | - Log in with your test account at https://accounts.google.com/ 95 | - Access the Google Developers Console: https://console.developers.google.com 96 | - Create a project 97 | - Access the project you just created 98 | - In the left three-bar/hamburger menu, open "API Manager" 99 | - Select the Credentials link on the left 100 | - Create an OAuth2.0 Client ID of type "Other", native has been renamed to 101 | "Other" in many places. 102 | - Two tokens are needed: under Client ID for native applications, copy the 103 | -- Client ID 104 | -- Client Secret 105 | 106 | 2. Edit _config.py 107 | 108 | The following sections should be edited: 109 | 110 | -- AUTOMODE -- 111 | * You should probably set this to False, as this will cause each 112 | print test to wait for user input to determine if it passed or failed. If 113 | AUTOMODE = True, then all jobs will be printed out and regardless of how they 114 | look, the test will pass. 115 | 116 | -- CAPS -- 117 | * This should reflect the actual capabilities of the printer. Set the 118 | following values to True or False, depending on if the printer supports them 119 | or not. 120 | 121 | Please refer to _config.py for instructions to populating these fields. 122 | 123 | -- SLEEP -- 124 | * This script tries its best to avoid using sleep but in the case that it is 125 | used, you can tweak the sleep times based on the categories. The printer 126 | specific ones are REGISTRATION, and PRINTER_STATE, defaults are 5 and 10 secs 127 | respectively. 128 | 129 | -- TEST['RUN'] -- 130 | * You may specify which tests to run/ignore via editing this. 131 | Add a '#' prefix in front of a test name to exclude from test 132 | . 133 | 134 | -- LOGFILES -- 135 | * Change this from /tmp/logocert if you want the log files to be stored in a 136 | different location. 137 | 138 | -- PRINTER -- 139 | * Add the correct values for CERTID (Certification ID), FIRMWARE, IP address, 140 | MANUFACTURER, MODEL, PORT (integer, not string), SERIAL, and STATUS. 141 | 142 | -- TEST -- 143 | * If you don't want the test results to be written to a Google Spreadsheet, 144 | then change SPREADSHEET to False. 145 | 146 | * If you don't want to share the Google Sheets results with Google, set 147 | SHARE_SHEET_WITH_GOOGLE to False. However, it is highly recommended that you 148 | enable sharing to facilitate debugging and progress tracking. Setting 149 | SHARE_SHEET_WITH_GOOGLE to True enables read/write access for 150 | cloud-print-certification-team@google.com. 151 | 152 | * If you are on a Windows machine and running the tool out of an ANSI color 153 | supporting console and want to enable color output, set FORCE_COLOR_OUTPUT 154 | to True. 155 | 156 | -- USER -- 157 | * Add the client id, client secret of the test account from step 1. Also add the 158 | email address of this user. 159 | 160 | -- USER2 -- 161 | * Add the email address of the 2nd test account. 162 | 163 | Save the _config.py and then all of the preparatory work is completed. Now simply 164 | execute testcert.py (NOTE: The user account should have no GCP printers registered 165 | under it before this script runs): 166 | 167 | ./testcert.py 168 | 169 | Note that the first time you run this script, you may need to enter your 170 | credentials and sign in manually and click to authorize permissions. During the 171 | tests, sometimes you'll be prompted to accept printer registration on the 172 | device, and other times to ignore or cancel registration requests. Pay attention 173 | to the testcert.py output as it will ask you to turn the printer on and off at 174 | various times during the test process. All prompt actions are highlighted and 175 | create a beep sound when they appear. 176 | 177 | If you need to remove some of the test suites, edit _config.py (TEST['RUN']), 178 | and add a '#' in front of suite names you don't want to run. Pay attention 179 | to the whether the printer is registered or not before isolating suites however. 180 | See the list below for the required initial condition of each suite. 181 | 182 | The complete list of all testsuites and the order they should run is 183 | as follows: 184 | 185 | -------------------------Printer is UNREGISTERED before running the suites below 186 | - SystemUnderTest 187 | - Privet 188 | - PreRegistration 189 | - Registration 190 | -------------------------Printer is REGISTERED before running the suites below 191 | - PostRegistration 192 | - LocalDiscovery 193 | - Printer 194 | - PrinterState 195 | - JobState 196 | - CloudPrinting 197 | - LocalPrinting 198 | - RunAfter24Hours 199 | - Unregister 200 | -------------------------Printer is UNREGISTERED before running the suites below 201 | - PostUnregistration 202 | 203 | The order that the test cases run is determined alphanumerically, but the suites 204 | will run in the order they are placed in. 205 | 206 | LocalPrinting has a one-time dependency on CloudPrinting. When the 207 | LocalPrinting suite is run for the first time for a printer, it will make calls 208 | to the GCP submit interface in order to download the converted pwg-raster images 209 | that will be used in subsequent LocalPrinting tests. If Cloud Printing does not 210 | work for your printer, you would have to manually generate pwg-raster formats of 211 | testpage.png and rosemary.pdf then store them under the images directory as 212 | testpage.pwg and rosemary.pwg. 213 | 214 | Once the test has run, results will be placed in a log file. Log files are 215 | created with a date-time stamp in the logname. All of the test results will be 216 | annotated with passed, skipped, blocked, or failed, and possibly some other 217 | debug information. And if TEST['SPREADSHEET'] = True, then the results will be 218 | placed in a Google spreadsheet for easy reading of the test results. 219 | 220 | For each test that are blocked or failed, a cmdline is provided in the Google 221 | spreadsheet for the individual test case to be run again only by itself. 222 | They follow this format: 223 | python -m unittest [moduleName].[testSuiteName].[testCaseName] 224 | Example: 225 | python -m unittest testcert.Privet.testDeviceRegistrationInvalidClaimToken 226 | 227 | Known Issues 228 | 229 | 1. Mac OSX 230 | 231 | If running on Mac OSX, you may come across this error - 'AttributeError: 232 | 'Module_six_moves_urllib_parse' object has no attribute 'urlparse'' This occurs 233 | due to OSX comes with an out-of-date six.py. To fix this, simply run the 234 | following command each time you open a new terminal or add to bash profile: 235 | 236 | export PYTHONPATH=/Library/Python/2.7/site-packages 237 | 238 | This will make python to check site-packages before the OSX system dirs 239 | 240 | 2. Windows 241 | 242 | If running on Windows, you may not be able to detect any printers on start 243 | up. This is due to an existing zeroconf issue: 244 | https://github.com/jstasiak/python-zeroconf/issues/84 245 | 246 | There are two workarounds for this: 247 | 248 | a) Comment out the line "if addr.get('netmask') != HOST_ONLY_NETWORK_MASK" in 249 | zeroconf.py (Note: This requires editing a Python dependency) 250 | 251 | OR 252 | 253 | b) Downgrading zeroconf.py's dependency, netifaces, to 0.10.4 254 | 255 | -------------------------------------------------------------------------------- /_common.py: -------------------------------------------------------------------------------- 1 | """Copyright 2016 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Common functions and utilities used across the logocert package. 17 | 18 | This provides some common functions and classes that are needed by multiple 19 | classes and modules in the Logo Certification Package. 20 | """ 21 | 22 | import time 23 | import os 24 | from _config import Constants 25 | 26 | # Module level variable 27 | _use_color_output = (Constants.TEST['FORCE_COLOR_OUTPUT'] or 28 | 'nt' not in os.name.lower()) 29 | 30 | def Sleep(wait_type): 31 | sec = Constants.SLEEP[wait_type] 32 | if 'POLL' not in wait_type: 33 | print '[Configurable sleep] %s: %s seconds' %(wait_type, sec) 34 | time.sleep(sec) 35 | 36 | def GreenText(str): 37 | """Display text in green 38 | 39 | Args: 40 | str: string, the str to display, cannot be None. 41 | """ 42 | global _use_color_output 43 | 44 | return str if not _use_color_output else '\033[92m'+str+'\033[0m' 45 | 46 | def RedText(str): 47 | """Display text in red 48 | 49 | Args: 50 | str: string, the str to display, cannot be None. 51 | """ 52 | global _use_color_output 53 | 54 | return str if not _use_color_output else '\033[91m'+str+'\033[0m' 55 | 56 | def BlueText(str): 57 | """Display text in blue 58 | 59 | Args: 60 | str: string, the str to display, cannot be None. 61 | """ 62 | global _use_color_output 63 | 64 | return str if not _use_color_output else '\033[94m'+str+'\033[0m' 65 | 66 | def PurpleText(str): 67 | """Display text in purple 68 | 69 | Args: 70 | str: string, the str to display, cannot be None. 71 | """ 72 | global _use_color_output 73 | 74 | return str if not _use_color_output else '\033[95m' + str + '\033[0m' 75 | 76 | def PromptUserAction(msg): 77 | """Display text in warning color and beep 78 | 79 | Args: 80 | msg: string, the msg to prompt the user. 81 | Returns: 82 | string, prompt string 83 | """ 84 | print "\a" # Cross-platform beep 85 | print PurpleText('[ACTION] '+msg) 86 | 87 | def PromptAndWaitForUserAction(msg): 88 | """Display text in green and beep - cross-platform, then wait for user to 89 | press enter before continuing 90 | 91 | Args: 92 | msg: string, the msg to prompt the user. 93 | Returns: 94 | string, user input string 95 | """ 96 | PromptUserAction(msg) 97 | return raw_input(PurpleText('>>> ')) 98 | 99 | 100 | def Extract(dict_in, dict_out): 101 | """Extract all the keys and values from a nested dictionary. 102 | 103 | Args: 104 | dict_in: dictionary of unknown size and levels. 105 | dict_out: dictionary to be created. 106 | """ 107 | if isinstance(dict_in, dict): 108 | for key, value in dict_in.iteritems(): 109 | if isinstance(value, dict): 110 | Extract(value, dict_out) 111 | elif isinstance(dict_in, list): 112 | for i in dict_in: 113 | Extract(i, dict_out) 114 | else: 115 | dict_out[key] = value 116 | else: 117 | type(dict_in) 118 | 119 | 120 | class Error(Exception): 121 | """Base class for exceptions in this module. 122 | 123 | Args: 124 | expr: string, expression that caused the error. 125 | msg: string, reason for error. 126 | """ 127 | 128 | def __init__(self, expr, msg): 129 | super(Error, self).__init__() 130 | self.expr = expr 131 | self.msg = msg 132 | 133 | 134 | class InitError(Error): 135 | """Exception raised for errors in the class initialization. 136 | 137 | Args: 138 | msg: string, reason for error. 139 | """ 140 | 141 | def __init__(self, msg): 142 | super(InitError, self).__init__('Error initializing object.', msg) 143 | -------------------------------------------------------------------------------- /_config.py: -------------------------------------------------------------------------------- 1 | """Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Package wide constants and variables used by the Logo Cert pacakge. 17 | 18 | Use this module to support need constants and variables across multiple 19 | modules or classes. There are also some common functions defined here. 20 | """ 21 | 22 | import datetime 23 | import os 24 | 25 | 26 | class Constants(object): 27 | """A classs that holds constants for the Logo Certification tool.""" 28 | 29 | ACCOUNTS = 'https://accounts.google.com' 30 | 31 | AUTH = { 32 | 'CRED_FILE': 'credentials.json', 33 | 'REDIRECT': 'urn:ietf:wg:oauth:2.0:oob', 34 | 'SCOPE': ('https://www.googleapis.com/auth/cloudprint ' 35 | 'https://spreadsheets.google.com/feeds/ ' 36 | 'https://www.googleapis.com/auth/drive'), 37 | 'USER_AGENT': 'CloudPrint_Client', 38 | } 39 | 40 | # AUTOMODE determines if manual input is needed for some test results. 41 | # If a user must examine a print job to determine pass or fail, set 42 | # AUTOMODE to False. 43 | AUTOMODE = False 44 | 45 | # CAPS represent device features and affects which tests will be run. 46 | # The values should be True or False. 47 | CAPS = { 48 | 'COLLATE': False, 49 | 'COLOR': False, 50 | 'COPIES_LOCAL': True, # If a Printer supports copies for local 51 | # printing, set this to True. 52 | 'COPIES_CLOUD': True, # If a Printer supports copies for cloud 53 | # printing, set this to True. 54 | 'COVER': True, # Not all printers have a cover. 55 | 'DUPLEX': True, 56 | 'LAYOUT_ISSUE': True, # Printer must set page orientation for local 57 | # printing, so page_orientation is still needed 58 | # in the printer capabilities. 59 | 'LOCAL_PRINT_WITHOUT_REG': True, # If a Printer supports local printing 60 | # without being registered, set this to True. 61 | 'TONER': True, # Set to false if printer is thermal or 62 | # has no toner. 63 | 'TRAY_SENSOR': False, # Set this to True if printer has a sensor to 64 | # detect if the paper tray is open. 65 | 'MEDIA_SENSOR': False, # Set this to True if printer has a sensor to 66 | # detect if the paper tray is empty. 67 | 'MARGIN': True, # Set this to True if a Printer supports 68 | # margin specifications 69 | 'GOODBYE_PACKET': True, # Set this to True if a printer supports 70 | # sending a Privet goodbye packet when the 71 | # device is shut off. 72 | 'EMPTY_INK_SENSOR': False, # Set this to True if a printer can sense 73 | # when the ink/toner is empty 74 | 'PRINTER_PANEL_UI': True, # Set this to True if a printer supports 75 | # registration interaction through panel UI 76 | 'WEB_URL_UI': False, # Set this to True if a printer supports 77 | # registration interaction through Web URL UI 78 | 'CONVERSION_PRINT': True, # Set this to True if a printer supports 79 | # the conversion printing for local print jobs 80 | } 81 | 82 | # Sleep contains the various durations of time.sleep() in SECONDS 83 | # that are used in this suite of scripts 84 | SLEEP = { 85 | 'POLL' : 1, # Used inside loops to space out polls to 86 | # GCP/Privet API's 87 | 'REG_CANCEL': 5, # Printer dependent- used after user cancels 88 | # registration request on the printer 89 | 'PRINTER_STATE': 10, # Printer dependent: used when printer state is 90 | # altered physically e.g. Paper tray open/close, 91 | # ink catridge remove/replaced 92 | 'NETWORK_DETECTION': 60, # Printer dependent: used when printer is 93 | # connected or disconnected to a network 94 | 'ONE_DAY': 86400, # 24 hrs in seconds 95 | 'AUTO_RUN': 5 # Used if AUTOMODE is set to be true 96 | 97 | } 98 | 99 | # TIMEOUT contains the various durations of maximum wait times in SECONDS 100 | # for polling waits. 101 | # Used for setting the upperbound for event-driven polling waits 102 | TIMEOUT = { 103 | 'GCP_UPDATE': 60, # Used for waiting on GCP update requests to 104 | # change state from pending to current 105 | 'PRINTER_STATUS': 180, # Used for waiting for the printer to have a 106 | # certain status: processing, idle, etc 107 | # Statuses come from the device's Privet/info 108 | # interface 109 | 'PRINTING': 1000, # Printer dependent: Used for waiting for a 110 | # printer to complete printing a job. 111 | } 112 | 113 | GCP = {'MGT': 'https://www.google.com/cloudprint'} 114 | 115 | PRINTER = { 116 | 'CERTID': '', 117 | 'FIRMWARE': '', 118 | 'IP': '', 119 | 'MANUFACTURER': '', 120 | 'MODEL': '', 121 | 'NAME': '', 122 | 'PORT': '', 123 | 'SERIAL': '', 124 | 'STATUS': '', 125 | } 126 | 127 | image_dir = os.path.join(os.getcwd(), 'images') 128 | IMAGES = { 129 | 'GIF1': os.path.join(image_dir, '6MB.gif'), 130 | 'GIF2': os.path.join(image_dir, 'img_0012.gif'), 131 | 'GIF3': os.path.join(image_dir, 'poster.gif'), 132 | 'GIF4': os.path.join(image_dir, 'Google-Glass.gif'), 133 | 'HTML1': os.path.join(image_dir, 'ChromeOSPowerManagementSpec.html'), 134 | 'JPG1': os.path.join(image_dir, 'b&w-test.jpg'), 135 | 'JPG2': os.path.join(image_dir, 'colorkey.jpg'), 136 | 'JPG3': os.path.join(image_dir, 'Chartley_Castle-1.jpg'), 137 | 'JPG4': os.path.join(image_dir, 'GoogleCampus.jpg'), 138 | 'JPG5': os.path.join(image_dir, 'google-car.jpg'), 139 | 'JPG6': os.path.join(image_dir, 'GoogleGlass.jpg'), 140 | 'JPG7': os.path.join(image_dir, 'GoogleGlass2.jpg'), 141 | 'JPG8': os.path.join(image_dir, 'landscape-test.jpg'), 142 | 'JPG9': os.path.join(image_dir, 'largeref.jpg'), 143 | 'JPG10': os.path.join(image_dir, 'max_test_big.jpg'), 144 | 'JPG11': os.path.join(image_dir, 'multitarget5.jpg'), 145 | 'JPG12': os.path.join(image_dir, 'brin.jpg'), 146 | 'JPG13': os.path.join(image_dir, 'stepchart.jpg'), 147 | 'JPG14': os.path.join(image_dir, 'testprint.jpeg'), 148 | 'PDF1': os.path.join(image_dir, 'a3color.pdf'), 149 | 'PDF1.2': os.path.join(image_dir, 'PDF1.2.pdf'), 150 | 'PDF1.3': os.path.join(image_dir, 'PDF1.3.pdf'), 151 | 'PDF1.4': os.path.join(image_dir, 'PDF1.4.pdf'), 152 | 'PDF1.5': os.path.join(image_dir, 'PDF1.5.pdf'), 153 | 'PDF1.6': os.path.join(image_dir, 'PDF1.6.pdf'), 154 | 'PDF1.7': os.path.join(image_dir, 'PDF1.7.pdf'), 155 | 'PDF2': os.path.join(image_dir, 'boardingpass.pdf'), 156 | 'PDF3': os.path.join(image_dir, 'letter_p.pdf'), 157 | 'PDF4': os.path.join(image_dir, 'lorem.pdf'), 158 | 'PDF5': os.path.join(image_dir, 'malformatted.pdf'), 159 | 'PDF6': os.path.join(image_dir, 'margin-test.pdf'), 160 | 'PDF7': os.path.join(image_dir, 'noise.pdf'), 161 | 'PDF8': os.path.join(image_dir, 'pickrpt.pdf'), 162 | 'PDF9': os.path.join(image_dir, 'printtest.pdf'), 163 | 'PDF10': os.path.join(image_dir, 'rosemary.pdf'), 164 | 'PDF11': os.path.join(image_dir, 'Satake_AE_web.pdf'), 165 | 'PDF12': os.path.join(image_dir, 'ticket.pdf'), 166 | 'PDF13': os.path.join(image_dir, 'version4pdf.pdf'), 167 | 'PDF14': os.path.join(image_dir, 'YourTickets.pdf'), 168 | 'PNG1': os.path.join(image_dir, 'A4testpage.png'), 169 | 'PNG2': os.path.join(image_dir, 'dna_overview.png'), 170 | 'PNG3': os.path.join(image_dir, 'gcpbeta.png'), 171 | 'PNG4': os.path.join(image_dir, 'google_logo.png'), 172 | 'PNG5': os.path.join(image_dir, 'printtest.png'), 173 | 'PNG6': os.path.join(image_dir, 'printtest2.png'), 174 | 'PNG7': os.path.join(image_dir, 'testpage.png'), 175 | 'PNG8': os.path.join(image_dir, 'larrypage.png'), 176 | 'PNG9': os.path.join(image_dir, 'mandlebulb_3d_test.png'), 177 | 'SVG1': os.path.join(image_dir, 'DoNotDisturb.svg'), 178 | 'SVG2': os.path.join(image_dir, 'Example.svg'), 179 | 'TIFF1': os.path.join(image_dir, 'gcpreglink.tif'), 180 | 'TIFF2': os.path.join(image_dir, 'marbles.tif'), 181 | # Below are in pwg-raster format, 182 | # one-time generated by the local printing suite 183 | 'PWG1': os.path.join(image_dir, '-'.join([PRINTER['MODEL'], 184 | PRINTER['NAME'], 185 | 'testpage.pwg'])), 186 | 'PWG2': os.path.join(image_dir, '-'.join([PRINTER['MODEL'], 187 | PRINTER['NAME'], 188 | 'rosemary.pwg'])), 189 | } 190 | 191 | LOGFILES = '/tmp/logocert/' 192 | 193 | OAUTH = 'https://accounts.google.com/o/oauth2/auth' 194 | OAUTH_TOKEN = 'https://www.googleapis.com/oauth2/v3/token' 195 | 196 | TEST = { 197 | # Please use descriptive names for sheets shared with Google 198 | # (e.g. Model-PrinterName, or PrinterName-FirmwareVersion) 199 | 'NAME': '_'.join([PRINTER['MODEL'], 200 | PRINTER['NAME'], 201 | str(datetime.date.today())]), 202 | 'RESULTS': ['Test Case ID', 'Test Case Name', 'Status', 'Notes', 203 | '','','','Re-run Cmd line'], 204 | 'SPREADSHEET': True, 205 | # It is recommended that you set the below to True so that Google's 206 | # GCP certification team can better debug issues relating to your printer 207 | 'SHARE_SHEET_WITH_GOOGLE': True, 208 | 'GCP_TEAM_EMAIL': 'cloud-print-certification-team@google.com', 209 | # Set the following to True if you wish to enable console output coloring 210 | # on Windows. Note that you would need an ANSI color supporting console 211 | # for this or else strange characters would be printed 212 | 'FORCE_COLOR_OUTPUT': False, 213 | 'RUN':[ # Order matters, prefix with '#' to skip the test 214 | # Update this each time new test classes are added or 215 | # class names have changed in testcert.py 216 | 'SystemUnderTest', 217 | 'Privet', 218 | 'PreRegistration', 219 | 'Registration', 220 | 'PostRegistration', 221 | 'LocalDiscovery', 222 | 'Printer', 223 | 'PrinterState', 224 | 'JobState', 225 | 'CloudPrinting', 226 | 'LocalPrinting', 227 | 'RunAfter24Hours', 228 | 'Unregister', 229 | 'PostUnregistration', 230 | ], 231 | } 232 | 233 | URL = { 234 | 'TIMEOUT': 20, 235 | } 236 | 237 | USER = { 238 | 'CLIENT_ID': '', 239 | 'CLIENT_SECRET': '', 240 | 'EMAIL': '', 241 | } 242 | 243 | USER2 = { 244 | 'EMAIL': '', 245 | } 246 | -------------------------------------------------------------------------------- /_cpslib.py: -------------------------------------------------------------------------------- 1 | """Copyright 2016 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Methods to interact with the Google Cloud Print Service APIs. 17 | 18 | GCPService will provide methods to access all of Cloud Print's Interfaces: 19 | 20 | delete (delete printer) 21 | deletejob (delete print job) 22 | jobs (get print jobs) 23 | list (list printers belonging to a specific proxy) 24 | printer (get printer capabilities and info) 25 | register (register a printer) 26 | search (search for printers and return basic information) 27 | submit (submit a print job) 28 | 29 | These interfaces are not used in the library, as they are printer specific: 30 | 31 | control (used by printer to update job state) 32 | fetch (used by printer to get the next print job) 33 | update (used by printer to update printer attributes) 34 | share (used by applications and interacts with GCP, not printers) 35 | unshare (used by applications and only interacts with GCP, not printers) 36 | 37 | 38 | This module is dependent on modules from the LogoCert package. 39 | """ 40 | from _common import Sleep 41 | from _common import Extract 42 | from _config import Constants 43 | from _transport import Transport 44 | 45 | from json import dumps 46 | from os.path import basename 47 | import requests 48 | import mimetypes 49 | import time 50 | import _log 51 | 52 | class GCPService(object): 53 | """Send and receive network messages and communication.""" 54 | 55 | def __init__(self, auth_token): 56 | """Get a reference to a logger object. 57 | 58 | Args: 59 | auth_token: the authentication token to use for GCP requests 60 | """ 61 | self.auth_token = auth_token 62 | self.logger = _log.GetLogger('LogoCert') 63 | self.transport = Transport(self.logger) 64 | 65 | 66 | def VerifyNotNone(query): 67 | """Decorator to check that None is not returned. 68 | This keeps calling code cleaner 69 | 70 | Args: 71 | query: function we are wrapping. 72 | Returns: 73 | formatted data from query if valid, otherwise raise exception 74 | """ 75 | def VerifyNotNone(self, *args, **kwargs): 76 | res = query(self, *args, **kwargs) 77 | if res is None: 78 | print '%s failed' % str(query) 79 | raise AssertionError 80 | return res 81 | 82 | return VerifyNotNone 83 | 84 | 85 | def HTTPGetQuery(query): 86 | """Decorator for various queries to GCP interfaces 87 | 88 | Args: 89 | query: function we are wrapping. 90 | Returns: 91 | formatted data from query to GCP Service Interface. 92 | """ 93 | def GCPQuery(self, *args, **kwargs): 94 | url = query(self, *args, **kwargs) 95 | headers = {'Authorization': 'Bearer %s' % self.auth_token} 96 | res = self.transport.HTTPGet(url, headers=headers) 97 | 98 | response_dict = {} 99 | Extract(res.json(), response_dict) 100 | return response_dict 101 | 102 | return GCPQuery 103 | 104 | @VerifyNotNone 105 | def FetchRaster(self, job_id): 106 | """Get the data content belonging to a job_id in pwg-raster format 107 | Note: This only works for job_id's that are queued, or in_progress. 108 | This will not work for jobs that have finished 109 | 110 | Args: 111 | job_id: string, printer's id 112 | Returns: 113 | str, the content in pwg-raster format if successful, otherwise, None 114 | 115 | """ 116 | url = '%s/download?id=%s&forcepwg=1' % (Constants.GCP['MGT'], job_id) 117 | headers = {'Authorization': 'Bearer %s' % self.auth_token} 118 | r = self.transport.HTTPGet(url, headers=headers) 119 | 120 | if r is None or requests.codes.ok != r.status_code: 121 | if r is None: 122 | print 'ERROR! Request to /download returned None type' 123 | elif r.status_code == 415: 124 | print ('GCP failed to provide raster file conversion, either supply ' 125 | 'your own raster files or capture the content via wireshark ' 126 | 'from manual printing the following from Chrome: testpage.png, ' 127 | 'rosemary.pdf, dna_overview.png') 128 | else: 129 | print ('ERROR! Bad HTTP status code received from /download: %s' % 130 | r.status_code) 131 | return None 132 | 133 | return r.content 134 | 135 | 136 | # Not decorated with @HTTPGetQuery since Submit() is an HTTP Post 137 | @VerifyNotNone 138 | def Submit(self, printer_id, content, title, cjt=None, is_url=False): 139 | """Submit a print job to the printer 140 | 141 | Args: 142 | printer_id: string, target printer to print from. 143 | content: string, url or absolute filepath of the item to print. 144 | title: string, title of the print job. 145 | cjt: CloudJobTicket, object that defines the options of the print job 146 | is_url: boolean, flag to identify between url's and files 147 | Returns: 148 | dictionary, response msg from the printer if successful; 149 | otherwise, None 150 | """ 151 | 152 | if cjt is None: 153 | cjt = {} 154 | else: 155 | cjt = cjt.val 156 | 157 | name = content 158 | 159 | if not is_url: 160 | name = basename(content) 161 | with open(content, 'rb') as f: 162 | content = f.read() 163 | 164 | if title is None: 165 | title = "LogoCert Testing: " + name 166 | 167 | content_type = 'url' if is_url else mimetypes.guess_type(name)[0] 168 | files = {"content": (name,content)} 169 | url = '%s/submit' % (Constants.GCP['MGT']) 170 | headers = {'Authorization': 'Bearer %s' % self.auth_token} 171 | 172 | data = {'printerid': printer_id, 173 | 'title': title, 174 | 'contentType': content_type, 175 | 'ticket': dumps(cjt)} 176 | 177 | # Depending on network speed, large files may take a while to submit 178 | print 'Attempting to submit a job through GCP for up to 60 seconds' 179 | t_end = time.time()+60 180 | while time.time() < t_end: 181 | r = self.transport.HTTPPost(url, data=data, files=files, headers=headers) 182 | if r is None: 183 | print 'ERROR! HTTP POST to /submit returned None type' 184 | return None 185 | elif r.status_code == requests.codes.ok: 186 | # Success 187 | res = r.json() 188 | # TODO may have to fuzzy match here, print job added may not be standard 189 | res['success'] = (res['success'] and 190 | 'print job added' in res['message'].lower()) 191 | 192 | if res['success']: 193 | print 'Job submitted successfully' 194 | else: 195 | print 'success: %s, msg: %s' % (res['success'], res['message']) 196 | return res 197 | else: 198 | # Try again if we get HTTP error code 199 | print 'Bad status code from Submit(): %s' % r.status_code 200 | if r.status_code == requests.codes.forbidden: 201 | # This should not happen, calling code should manage token refresh 202 | self.logger.info('Access token expired, need to refresh it.') 203 | print 'Trying again in %s sec(s)' % Constants.SLEEP['POLL'] 204 | Sleep('POLL') 205 | # Continuously gotten HTTP error codes to fall out of the while loop 206 | return None 207 | 208 | # Not decorated with @HTTPGetQuery since Update() is an HTTP Post 209 | @VerifyNotNone 210 | def Update(self, printer_id, setting): 211 | """Update a cloud printer 212 | 213 | Args: 214 | printer_id: string, target printer to update. 215 | setting: dict, local settings structure that describes the fields 216 | to update 217 | Returns: 218 | dictionary, response msg from the printer 219 | """ 220 | url = '%s/update' % (Constants.GCP['MGT']) 221 | headers = {'Authorization': 'Bearer %s' % self.auth_token} 222 | 223 | data = {'printerid': printer_id, 224 | 'local_settings': dumps(setting)} 225 | 226 | r = self.transport.HTTPPost(url, data=data, headers=headers) 227 | 228 | if r is None or requests.codes.ok != r.status_code: 229 | return False 230 | 231 | res = r.json() 232 | # TODO may have to fuzzy match here, print job added may not be a standard 233 | res['success'] = (res['success'] and 234 | 'printer updated successfully' in res['message'].lower()) 235 | return res 236 | 237 | 238 | @VerifyNotNone 239 | @HTTPGetQuery 240 | def Delete(self, printer_id): 241 | """Delete a printer owned by a user. 242 | 243 | Args: 244 | printer_id: string, printerid of registered printer. 245 | Returns: 246 | url: string, url to delete printer. 247 | """ 248 | url = '%s/delete?printerid=%s' % (Constants.GCP['MGT'], printer_id) 249 | 250 | return url 251 | 252 | @VerifyNotNone 253 | @HTTPGetQuery 254 | def DeleteJob(self, job_id): 255 | """Delete a job owned by user. 256 | 257 | Args: 258 | job_id: string, jobid of existing job owned by user. 259 | Returns: 260 | url: string, url to delete job. 261 | """ 262 | url = '%s/deletejob?jobid=%s' % (Constants.GCP['MGT'], job_id) 263 | 264 | return url 265 | 266 | @VerifyNotNone 267 | @HTTPGetQuery 268 | def Jobs(self, printer_id=None, owner=None, job_title=None, status=None): 269 | """Get a list of print jobs which user has permission to view. 270 | 271 | Args: 272 | printer_id: string, filter jobs sent to this printer. 273 | owner: string, filter jobs submitted by this owner. 274 | job_title: string, filter jobs whose title or tags contain this string. 275 | status: string, filter jobs that match this status. 276 | Returns: 277 | string, url to be used by HTTPGetQuery method. 278 | Valid Job state strings are: QUEUED, IN_PROGRESS, DONE, ERROR, SUBMITTED, 279 | and HELD. 280 | """ 281 | args = '?' 282 | url = '%s/jobs' % Constants.GCP['MGT'] 283 | if printer_id: 284 | url += '?printerid=%s' % printer_id 285 | args = '&' 286 | if owner: 287 | url += '%sowner=%s' % (args, owner) 288 | args = '&' 289 | if status: 290 | url += '%sstatus=%s' % (args, status) 291 | args= '&' 292 | if job_title: 293 | url += '%sq=%s' % (args, job_title) 294 | 295 | return url 296 | 297 | @VerifyNotNone 298 | @HTTPGetQuery 299 | def List(self, proxy_id): 300 | """Execute the list interface and return printer fields. 301 | 302 | Args: 303 | proxy_id: string, proxy of printer. 304 | Returns: 305 | string: url to by used by HTTPGetQuery method. 306 | Note: the List interface returns the same information as the Search 307 | interface; therefore, use the Search interface unless you need a list 308 | or printers using the same proxy_id. 309 | """ 310 | url = '%s/list?proxy=%s' % (Constants.GCP['MGT'], proxy_id) 311 | 312 | return url 313 | 314 | @VerifyNotNone 315 | @HTTPGetQuery 316 | def Printer(self, printer_id): 317 | """Execute the printer interface and return printer fields and capabilites. 318 | 319 | Args: 320 | printer_id: string, id of printer. 321 | Returns: 322 | string: url to be used by HTTPGetQuery method. 323 | """ 324 | fields = 'connectionStatus,semanticState,uiState,queuedJobsCount' 325 | url = '%s/printer?printerid=%s&usecdd=True&extra_fields=%s' % ( 326 | Constants.GCP['MGT'], printer_id, fields) 327 | 328 | return url 329 | 330 | @VerifyNotNone 331 | @HTTPGetQuery 332 | def Search(self, printer=None): 333 | """Search for printers owned by user. 334 | 335 | Args: 336 | printer: string, name or partial name of printer to search for. 337 | Returns: 338 | string: url to be used by HTTPGetQuery method. 339 | """ 340 | url = '%s/search' % Constants.GCP['MGT'] 341 | if printer: 342 | # replace all spaces with %20 343 | url += '?q=%s' % printer.replace(' ','%20') 344 | 345 | return url 346 | 347 | 348 | def __getJobFromList(self, job_list, job_id): 349 | """Find the specified job_id in a list of jobs 350 | 351 | Args: 352 | job_list: array, job objects. 353 | job_id: the job_id to look for 354 | Returns: 355 | object: the job object with the specified job_id 356 | """ 357 | for entry in job_list: 358 | if entry['id'] == job_id: 359 | return entry 360 | return None 361 | 362 | def GetJobInfo(self, job_id, printer_id, owner=None, job_title=None): 363 | """Find the specified job_id in from the Job query result 364 | 365 | Args: 366 | job_id: string, id of the print job. 367 | printer_id: string, id of the printer 368 | owner: string, filter jobs submitted by this owner. 369 | job_title: string, filter jobs whose title or tags contain this str. 370 | Returns: 371 | object: the job object with the specified job_id 372 | """ 373 | res = self.Jobs(printer_id=printer_id, owner=owner, job_title=job_title) 374 | job = self.__getJobFromList(res['jobs'], job_id) 375 | return job 376 | 377 | @VerifyNotNone 378 | def WaitJobStateNotIn(self, job_id, printer_id, job_state, timeout=60): 379 | """Wait until the job state is not the specified state. 380 | 381 | Args: 382 | job_id: string, id of the print job. 383 | printer_id: string, id of the printer 384 | job_state: string or list, job state(s) that should not be observed. 385 | timeout: integer, number of seconds to wait. 386 | Returns: 387 | string, current job. 388 | 389 | """ 390 | print ('Waiting up to %s seconds for the job to not have any of the ' 391 | 'following job state(s): %s\n' % (timeout, job_state)) 392 | 393 | end = time.time() + timeout 394 | 395 | while time.time() < end: 396 | job = self.GetJobInfo(job_id, printer_id) 397 | 398 | if job is not None: 399 | if job['semanticState']['state']['type'] not in job_state: 400 | return job 401 | 402 | Sleep('POLL') 403 | 404 | return None 405 | 406 | @VerifyNotNone 407 | def WaitJobStateIn(self, job_id, printer_id, job_state, timeout=60): 408 | """Wait until the job state becomes the specified state(s) 409 | 410 | Args: 411 | job_id: string, id of the print job. 412 | printer_id: string, id of the printer 413 | job_state: string or list, job state(s) to wait for. 414 | timeout: integer, number of seconds to wait. 415 | Returns: 416 | dict, current job. 417 | 418 | """ 419 | print ('Waiting up to %s seconds for the job to have one of the following ' 420 | 'job state(s): %s\n' % (timeout, job_state)) 421 | 422 | end = time.time() + timeout 423 | 424 | while time.time() < end: 425 | job = self.GetJobInfo(job_id, printer_id) 426 | 427 | if job is not None: 428 | if job['semanticState']['state']['type'] in job_state: 429 | return job 430 | 431 | Sleep('POLL') 432 | 433 | return None 434 | 435 | 436 | def WaitForUpdate(self, dev_id, key, expected_value, 437 | timeout=Constants.TIMEOUT['GCP_UPDATE']): 438 | '''Wait for the printer's local_settings attribute matches an expected value 439 | 440 | Args: 441 | dev_id: string, id of the printer. 442 | key: string, the local_settings attribute to poll for. 443 | expected_value: int or boolean, the expected value of the attribute. 444 | timeout: integer, number of seconds to wait. 445 | Returns: 446 | boolean, True if expected value is observed, otherwise False 447 | ''' 448 | print '[Configurable timeout] GCP_UPDATE:' 449 | print ('Waiting up to %s seconds for printer to accept pending settings' % 450 | timeout) 451 | 452 | end = time.time() + timeout 453 | 454 | while time.time() < end: 455 | # Continue to use the /Update to access the current local settings 456 | try: 457 | res = self.Update(dev_id,{}) 458 | except AssertionError: 459 | print 'GCP Update call failed' 460 | return False 461 | else: 462 | if key not in res['printer']['local_settings']['current']: 463 | print 'ERROR: "%s" does not exist in local_settings' % key 464 | return False 465 | cur_val = res['printer']['local_settings']['current'][key] 466 | if expected_value == cur_val: 467 | return True 468 | Sleep('POLL') 469 | return False 470 | 471 | 472 | def WaitLocalJobExist(self, printer_id, job_title, timeout=60): 473 | """Wait until the local print job is present in /job api. 474 | 475 | Args: 476 | printer_id: string, id of the printer 477 | job_title: string, title of the print job. 478 | timeout: integer, number of seconds to wait. 479 | Returns: 480 | boolean, True if job exists. 481 | 482 | """ 483 | print ('Waiting up to %s seconds for the local print job to be reported to ' 484 | 'GCP servers\n' % timeout) 485 | 486 | end = time.time() + timeout 487 | 488 | while time.time() < end: 489 | res = self.Jobs(printer_id=printer_id, job_title=job_title) 490 | if res['jobsCount'] > 0: 491 | return True 492 | 493 | Sleep('POLL') 494 | 495 | return False 496 | -------------------------------------------------------------------------------- /_device.py: -------------------------------------------------------------------------------- 1 | """Copyright 2016 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | A class to hold device attributes. 17 | 18 | This class is used by the Cloud Print Logo Certification tool, to hold the 19 | attributes of a device. Before the device attributes are fully populated, 20 | the methods GetDeviceDetails and GetDeviceCDD must be run. 21 | """ 22 | 23 | from _config import Constants 24 | from _privet import Privet 25 | from _transport import Transport 26 | 27 | from _common import Sleep 28 | from _common import RedText 29 | from _common import PromptUserAction 30 | from json import dumps 31 | import copy 32 | import requests 33 | import time 34 | 35 | 36 | 37 | class Device(object): 38 | """The basic device object.""" 39 | 40 | def __init__(self, logger, auth_token, gcp, model=None, privet_port=None): 41 | """Initialize a device object. 42 | 43 | Args: 44 | logger: initialized logger object. 45 | auth_token: string, auth_token of authenticated user. 46 | gcp: initialized GCPService object 47 | model: string, unique model or name of device. 48 | privet_port: integer, tcp port devices uses for Privet protocol. 49 | """ 50 | if model: 51 | self.model = model 52 | else: 53 | self.model = Constants.PRINTER['MODEL'] 54 | 55 | self.auth_token = auth_token 56 | self.logger = logger 57 | self.transport = Transport(logger) 58 | self.ipv4 = Constants.PRINTER['IP'] 59 | if privet_port: 60 | self.port = privet_port 61 | else: 62 | self.port = Constants.PRINTER['PORT'] 63 | self.dev_id = None 64 | self.name = Constants.PRINTER['NAME'] 65 | self.gcp = gcp 66 | self.status = None 67 | self.messages = {} 68 | self.details = {} 69 | self.error_state = False 70 | self.warning_state = False 71 | self.cdd = {} 72 | self.supported_types = None 73 | self.info = None 74 | 75 | self.url = 'http://%s:%s' % (self.ipv4, self.port) 76 | self.logger.info('Device URL: %s', self.url) 77 | self.transport = Transport(logger) 78 | self.headers = None 79 | self.privet = Privet(logger) 80 | self.privet_url = self.privet.SetPrivetUrls(self.ipv4, self.port) 81 | self.GetPrivetInfo() 82 | 83 | 84 | def GetPrivetInfo(self): 85 | self.privet_info = {} 86 | info = self.Info() 87 | if info is not None: 88 | for key in info: 89 | self.privet_info[key] = info[key] 90 | self.logger.debug('Privet Key: %s', key) 91 | self.logger.debug('Value: %s', info[key]) 92 | self.logger.debug('--------------------------') 93 | if 'x-privet-token' in info: 94 | self.headers = {'X-Privet-Token': str(info['x-privet-token'])} 95 | 96 | 97 | def Register(self, msg, user=Constants.USER['EMAIL'], use_token=True, 98 | no_action=False, wait_for_user=True): 99 | """Register device using Privet. 100 | Args: 101 | msg: string, the instruction for the user about the registration 102 | confirmation dialog on the printer 103 | user: string, the user to register for 104 | use_token: boolean, use auth_token if True 105 | no_action: boolean, if True, do not prompt with [ACTION] prefix 106 | wait_for_user: boolean, if True, wait for user to press UI button 107 | Returns: 108 | boolean: True = device registered, False = device not registered. 109 | Note, devices require user input to accept or deny a registration 110 | request, so manual intervention is required. 111 | """ 112 | if self.StartPrivetRegister(user=user): 113 | if no_action: 114 | print msg 115 | else: 116 | if Constants.CAPS['PRINTER_PANEL_UI'] or Constants.CAPS['WEB_URL_UI']: 117 | PromptUserAction(msg) 118 | if self.GetPrivetClaimToken(user=user, wait_for_user=wait_for_user): 119 | auth_token = self.auth_token if use_token else None 120 | if self.ConfirmRegistration(auth_token): 121 | self.FinishPrivetRegister() 122 | return True 123 | 124 | return False 125 | 126 | def detectRegisterCancel(self, msg, user=Constants.USER['EMAIL']): 127 | """Detect User Cancellation of Registration process using Privet. 128 | Args: 129 | msg: string, the instructional prompt for cancellation 130 | user: string, the user to register for 131 | Returns: 132 | boolean: True = device registration cancelled successfully 133 | False = device registration cancel failed 134 | """ 135 | if self.StartPrivetRegister(user=user): 136 | PromptUserAction(msg) 137 | 138 | print ('Waiting up to 60 seconds for printer UI interaction ' 139 | 'then getting Privet Claim Token.') 140 | t_end = time.time() + 60; 141 | while time.time() 0): 250 | self.error_state = True 251 | else: 252 | self.error_state = False 253 | return True 254 | else: 255 | self.logger.error('Could not find printers in cdd.') 256 | return False 257 | 258 | 259 | def __parseCDD(self, printer): 260 | """Parse the CDD json string into a logical dictionary. 261 | 262 | Args: 263 | printer: formatted data from /printer interface. 264 | Returns: 265 | boolean: True = CDD parsed, False = CDD not parsed. 266 | """ 267 | for k in printer: 268 | if k == 'capabilities': 269 | self.cdd['caps'] = {} 270 | else: 271 | self.cdd[k] = printer[k] 272 | 273 | for k in printer['capabilities']['printer']: 274 | self.cdd['caps'][k] = printer['capabilities']['printer'][k] 275 | return True 276 | 277 | def CancelRegistration(self): 278 | """Cancel Privet Registration that is in progress. 279 | 280 | Returns: 281 | return code from HTTP request. 282 | """ 283 | self.logger.debug('Sending request to cancel Privet Registration.') 284 | url = self.privet_url['register']['cancel'] 285 | params = {'user': Constants.USER['EMAIL']} 286 | r = self.transport.HTTPPost(url, headers=self.headers, params=params) 287 | 288 | if r is None: 289 | raise 290 | 291 | Sleep('REG_CANCEL') 292 | 293 | return r.status_code 294 | 295 | def StartPrivetRegister(self, user=Constants.USER['EMAIL']): 296 | """Start a device registration using the Privet protocol. 297 | 298 | Returns: 299 | boolean: True = success, False = errors. 300 | """ 301 | 302 | self.logger.debug('Registering device %s with Privet', self.ipv4) 303 | url = self.privet_url['register']['start'] 304 | params = {'user': user} 305 | r = self.transport.HTTPPost(url, headers=self.headers, params=params) 306 | 307 | if r is None: 308 | return False 309 | 310 | return r.status_code == requests.codes.ok 311 | 312 | def GetPrivetClaimToken(self, user=Constants.USER['EMAIL'], 313 | wait_for_user=True): 314 | """Wait for user interaction with the Printer's UI and get a 315 | Privet Claim Token. 316 | Raises EnvironmentError if the printer keeps returning 317 | 'pending_user_action' 318 | Args: 319 | user: string, email address to register under. 320 | wait_for_user: boolean, True if user is expected to interact with printer 321 | 322 | Returns: 323 | boolean: True = success, False = errors. 324 | """ 325 | print ('Waiting up to 60 seconds for printer UI interaction ' 326 | 'then getting Privet Claim Token.') 327 | t_end = time.time() + 60; 328 | while time.time() 0: 74 | self.logger.error('zeroconf_obj.get_service_info returned None, forces ' 75 | 'retry.') 76 | time.sleep(0.1) 77 | retries -= 1 78 | info = zeroconf_obj.get_service_info(service_type, name, timeout=10000) 79 | if info is not None: 80 | self._added_service_infos.append(copy.deepcopy(info)) 81 | self.lock.release() 82 | 83 | def services(self): 84 | self.lock.acquire() 85 | infos = copy.deepcopy(self._added_service_infos) 86 | self.lock.release() 87 | return infos 88 | 89 | def removed_services(self): 90 | self.lock.acquire() 91 | removed_service_names = copy.deepcopy(self._removed_service_names) 92 | self.lock.release() 93 | return removed_service_names 94 | 95 | 96 | def _find_zeroconf_threads(): 97 | """Find all living threads that were started by zeroconf. 98 | 99 | Returns: 100 | List of thread objects started by zeroconf that are currently alive 101 | according to threading.enumerate(). 102 | """ 103 | def is_zeroconf_thread(thread): 104 | zeroconf_thread_objs = [ 105 | zeroconf.Engine, 106 | zeroconf.Reaper, 107 | zeroconf.ServiceBrowser 108 | ] 109 | for obj in zeroconf_thread_objs: 110 | if isinstance(thread, obj): 111 | return True 112 | return False 113 | zeroconf_threads = filter(is_zeroconf_thread, threading.enumerate()) 114 | return zeroconf_threads 115 | 116 | 117 | # pylint: disable=dangerous-default-value 118 | # The default case, [] is explicitly handled, and common. 119 | def Wait_for_privet_mdns_service(t_seconds, service, logger, 120 | wifi_interfaces=[]): 121 | """Listens for t_seconds and returns an information object for each service. 122 | 123 | This is the primary interface to discover mDNS services. It blocks for 124 | t_seconds while listening, and returns a list of information objects, one 125 | for each service discovered. 126 | Args: 127 | t_seconds: Time to listen for mDNS records, in seconds. Floating point ok. 128 | service: The service to wait for, if found, return early 129 | is_add: If True, wait for service to be added 130 | If False, wait for service to be removed 131 | wifi_interfaces: The interfaces to listen on as strings, if empty listen on 132 | all interfaces. For example: ['192.168.1.2']. 133 | Returns: 134 | If Add event observed, return the Zeroconf information class; 135 | otherwise, return None 136 | """ 137 | l = _Listener(logger) 138 | if not wifi_interfaces: 139 | z = Zeroconf() 140 | else: 141 | z = Zeroconf(wifi_interfaces) 142 | 143 | sb = ServiceBrowser(zc=z, type_='_privet._tcp.local.', listener=l) 144 | service_info = wait_for_service_add(t_seconds, service, l) 145 | sb.cancel() 146 | 147 | # Only method available to kill all threads pylint: disable=protected-access 148 | z._GLOBAL_DONE = True 149 | zeroconf_threads = _find_zeroconf_threads() 150 | 151 | # Wait up to 30 seconds for zeroconf to terminate its threads 152 | t_end = time.time() + 30 153 | while len(zeroconf_threads) > 1 and time.time() < t_end: 154 | time.sleep(0.01) 155 | zeroconf_threads = _find_zeroconf_threads() 156 | z.close() 157 | 158 | if len(zeroconf_threads) > 1: 159 | logger.info('Zeroconf failed to terminate its threads in 30 seconds.') 160 | else: 161 | logger.info('All listeners have been stopped.') 162 | return service_info 163 | # pylint: enable=dangerous-default-value 164 | 165 | 166 | def wait_for_service_add(t_seconds, target_service, listener): 167 | """Wait for a service to be added. 168 | 169 | Args: 170 | t_seconds: Time to listen for mDNS records, in seconds. 171 | Floating point ok. 172 | service: string, The service to wait for 173 | listener: _Listener object, the listener to wait on 174 | Returns: 175 | If Add event observed, return the Zeroconf information class; otherwise, 176 | return None 177 | """ 178 | t_end = time.time() + t_seconds 179 | while time.time() < t_end: 180 | services = listener.services() 181 | for service in services: 182 | if target_service in service.properties['ty']: 183 | return service 184 | time.sleep(1) 185 | return None 186 | 187 | 188 | class MDNS_Browser: 189 | """Public class for this module. 190 | 191 | Used for keeping the service browser running until the user decides to 192 | stop it 193 | """ 194 | def __init__(self, logger, wifi_interfaces=[]): 195 | """Initialization requires a logger. 196 | 197 | Args: 198 | logger: initialized logger object. 199 | if_addr: string, interface address for Zeroconf, None means 200 | all interfaces. 201 | """ 202 | self.logger = logger 203 | self.l = _Listener(logger) 204 | if not wifi_interfaces: 205 | self.z = Zeroconf() 206 | else: 207 | self.z = Zeroconf(wifi_interfaces) 208 | self.sb = ServiceBrowser(zc=self.z, type_='_privet._tcp.local.', 209 | listener=self.l) 210 | 211 | def Wait_for_service_add(self, t_seconds, target_service): 212 | """Wait for a service to be added. 213 | 214 | Args: 215 | t_seconds: Time to listen for mDNS records, in seconds. 216 | Floating point ok. 217 | service: string, The service to wait for, if found, return early 218 | Returns: 219 | If Add event observed, return the Zeroconf information class; 220 | otherwise, return None 221 | """ 222 | return wait_for_service_add(t_seconds, target_service, self.l) 223 | 224 | def Wait_for_service_remove(self, t_seconds, target_service): 225 | """Wait for a service to be removed. 226 | 227 | Args: 228 | t_seconds: Time to listen for mDNS records, in seconds. 229 | Floating point ok. 230 | target_service: zeroconf service object, the service to wait for 231 | Returns: 232 | If Remove event observed, return the True; otherwise, return False 233 | """ 234 | t_end = time.time() + t_seconds 235 | while time.time() < t_end: 236 | services = self.l.removed_services() 237 | for service in services: 238 | if target_service.name == service: 239 | return True 240 | time.sleep(1) 241 | return False 242 | 243 | def Close(self): 244 | """Terminate the MDNS listening session by joining all threads""" 245 | self.sb.cancel() 246 | self.z._GLOBAL_DONE = True # Only method available to kill all 247 | # threads pylint: disable=protected-access 248 | zeroconf_threads = _find_zeroconf_threads() 249 | while len(zeroconf_threads) > 1: 250 | time.sleep(0.01) 251 | zeroconf_threads = _find_zeroconf_threads() 252 | self.z.close() 253 | self.logger.info('All listeners have been stopped.') 254 | return 255 | 256 | def Get_service_ttl(self, target_service): 257 | """Get the printer service's DNS record's TTL 258 | 259 | Args: 260 | target_service: zeroconf service object, service to get the TTL for. 261 | Returns: 262 | integer, TTL if service is found, None otherwise. 263 | """ 264 | for service in self.l.services(): 265 | if target_service.name == service.name: 266 | service_name = service.name.lower() 267 | return self.sb.services[service_name].get_remaining_ttl(time.time()* 1000) 268 | return None 269 | -------------------------------------------------------------------------------- /console_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/console_output.png -------------------------------------------------------------------------------- /images/6MB.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/6MB.gif -------------------------------------------------------------------------------- /images/A4testpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/A4testpage.png -------------------------------------------------------------------------------- /images/Chartley_Castle-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/Chartley_Castle-1.jpg -------------------------------------------------------------------------------- /images/ChromeOSPowerManagementSpec.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | Chrome OS Power Management Specification 20 | 21 |

Chrome OS Power Management Specification

22 | 23 |

Version: 1.0

24 | 25 |

Change History

26 | 27 |

05/17/12 .. V1.0 First version.

28 | 29 |

Terminology

30 | 31 |

Two form factors are defined for this document:

32 | 33 |
    34 |
  • Notebook: A complete system that contains an internal keyboard, 35 | display, and battery.
  • 36 |
  • Desktop: All complete systems that do not fall into the Notebook form 37 | factor.
  • 38 |
39 | 40 |

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 41 | "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 42 | interpreted as described in RFC 2119.

43 | 44 |

The key word "SUGGESTED" is used to indicate items that are optional but 45 | which enhance the user experience and are expected to be used in most 46 | systems.

47 | 48 |

Internal and External Peripheral Behavior

49 |

Notebook

50 |

The power policy for internal and external peripherals is as follows:

51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
On (AC)On (Battery)SuspendOff
DisplaySee Section 3.2.1See Section 3.1.1OffOff
BacklightSee Section 3.2.1See Section 3.1.1OffOff
WifiOnPower saving mode (module dependent)OffOff
3GOnPower M enabledOffOff
EthernetOnOnOffOff
USB (Charging, Wake, etc.)OnAuto-suspend for internal devices only after 2 seconds of 101 | inactivityOffOff
External Display PortOnOnOffOff
CPUOS-controlled frequency and low-power statesOS-controlled frequency and low-power statesSuspendOff
AudioLow power state* after 10s idleLow power after 10s idleOffOff
Storage (SATA)max_performancemin_power (SATA ALPM) or equivalent power saving modeOffOff
Internal KeyboardOnOnOnOff
TrackpadOnOnLow power modeOff
Embedded ControllerOnOnSleepDeep-sleep
156 | 157 | 158 |

* Audio low power state depends on device/driver. Example is D3 on 159 | x86/HDA.

160 | 161 |

Desktop

162 | 163 |

The following table details the current behavior of both internal and 164 | external peripherals for each power state on a desktop system.

165 | 166 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 |
OnSuspendOff
DisplaySee Section 3.2.2OffOff
BacklightSee Section 3.2.2OffOff
WifiOnOffOff
3GOnOffOff
EthernetOnOffOff
USB (Charging, Wake, etc.)OnAuto-suspend for internal devices onlyOff
CPUOS-controlled frequency and low-power statesSuspendOff
AudioLow power after 10s idleOffOff
Storagemax_performanceOffOff
Embedded ControllerOnSleepDeep-sleep
237 | 238 |

239 | 240 |

Power Timeout Values

241 | 242 | 243 |

Power Timeout Values on Battery

244 | 245 |

Notebook: Timeout values must meet the parameters specified below.
246 | Desktop: Not applicable.

247 | 248 |

Notebook

249 | 250 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 |
Dim screenScreen OffSuspend to RAM
Idle5m6m10m
Playing Audio5m6m-
Playing Video---
Projecting10m12m20m
285 | 286 |

Power policy timeouts are determined based on user data. Power policy may 287 | change if data about user interaction changes. All timeouts above are set to 288 | meet the interaction behaviors of 80 to 90% of users tested.

289 | 290 |

In a usage survey in early 2012:

291 | 292 |
    293 |
  • Approximately 80% of users will start using their device again within 5m of idle.
  • 294 |
  • Users complained when the screen dimming time was set at 3m.
  • 295 |
  • Approximately 90% of users act on the device within 1m after the screen has dimmed.
  • 296 |
  • Approximately 90% of users act on the device within 10m of going idle.
  • 297 |
298 | 299 |

Power Timeout Values on AC Power

300 | 301 |

Notebook

302 | 303 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
Dim screenScreen OffSuspend to RAM
Idle7m8m30m*
Playing Audio7m8m-
Playing Video---
Projecting14m16m60m
339 | 340 |

* Energy Star specifies 30m as the maximum allowed suspend time for default usage.

341 | 342 |

Desktop

343 | 344 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 |
Dim screenScreen OffSuspend to RAM
Idle7m8m30m
Playing Audio7m8m-
Playing Video---
373 | 374 |

Power policy is not changed for desktops that are outputting video on an 375 | external display, as this is the majority case.

376 | 377 |

Wake Sources

378 | 379 |

Notebook

380 | 381 |

The table below describes the expected supported wake sources in the 382 | different power states on a notebook. It is not the case that all of these will 383 | be implemented on the device.

384 | 385 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 397 | 398 | 399 | 400 | 401 | 402 | 403 |
Power StateWake Sources
Suspend to RAMPower Button
Lid Open
Internal 395 | Keyboard
Trackpad
RTC
USB (keyboard,mouse, 396 | insertion,removal)*
AC Power*
System offPower Button
Lid Open
404 | 405 |

* It must be possible to disable these wakes from the OS level.

406 | 407 |

Desktop

408 | 409 |

The table below describes the expected supported wake sources in the 410 | different power states on a desktop system. It is not the case that all of these 411 | will be utilized on the device.

412 | 413 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 424 | 425 | 426 | 427 | 428 | 429 | 430 |
Power StateWake Sources
Suspend to RAMPower Button
USB Keyboard
USB Mouse
RTC
USB 423 | Insertion/Removal*
Wake-on-LAN*
System offPower Button
431 | 432 |

* It must be possible to disable these wakes from the OS level.

433 | 434 |

USB Charging

435 | 436 |

All USB ports must default to being Standard Downstream Ports (SDP). To allow 437 | for higher power charging, it is suggested that systems support switching ports 438 | to be Charging Downstream Ports (CDP). Switching the state of ports is done 439 | under OS control (not automatically). The OS will identify devices that 440 | correctly implement the CDP protocol and enable fast charging for those devices. 441 | If multiple devices request charging, then the OS may limit total system power by 442 | only enabling fast charging for some of them.

443 | 444 |

Ports should not advertise themselves as Dedicated Charging Ports (DCP) 445 | because in all states where USB port power is provided, the USB data lines could 446 | be active.

447 | 448 |

On notebook systems, USB charging is enabled when the device is on, under OS 449 | control when in suspend, and powered down when the 450 | device is off.

451 | 452 |

On desktop systems, USB charging is permitted when the device is on or 453 | suspended, and the USB ports are powered down only when the system is off.

454 | -------------------------------------------------------------------------------- /images/DoNotDisturb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/Example.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 52 | 55 | 59 | 63 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /images/Google-Glass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/Google-Glass.gif -------------------------------------------------------------------------------- /images/GoogleArt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/GoogleArt.jpg -------------------------------------------------------------------------------- /images/GoogleCampus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/GoogleCampus.jpg -------------------------------------------------------------------------------- /images/GoogleGlass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/GoogleGlass.jpg -------------------------------------------------------------------------------- /images/GoogleGlass2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/GoogleGlass2.jpg -------------------------------------------------------------------------------- /images/PDF1.2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.2.pdf -------------------------------------------------------------------------------- /images/PDF1.3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.3.pdf -------------------------------------------------------------------------------- /images/PDF1.4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.4.pdf -------------------------------------------------------------------------------- /images/PDF1.5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.5.pdf -------------------------------------------------------------------------------- /images/PDF1.6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.6.pdf -------------------------------------------------------------------------------- /images/PDF1.7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/PDF1.7.pdf -------------------------------------------------------------------------------- /images/Satake_AE_web.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/Satake_AE_web.pdf -------------------------------------------------------------------------------- /images/YourTickets.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/YourTickets.pdf -------------------------------------------------------------------------------- /images/a3color.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/a3color.pdf -------------------------------------------------------------------------------- /images/b&w-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/b&w-test.jpg -------------------------------------------------------------------------------- /images/boardingpass.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/boardingpass.pdf -------------------------------------------------------------------------------- /images/brin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/brin.jpg -------------------------------------------------------------------------------- /images/colorkey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/colorkey.jpg -------------------------------------------------------------------------------- /images/dna_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/dna_overview.png -------------------------------------------------------------------------------- /images/fileinjpeg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/fileinjpeg.jpeg -------------------------------------------------------------------------------- /images/gcpbeta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/gcpbeta.png -------------------------------------------------------------------------------- /images/gcpreglink.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/gcpreglink.tif -------------------------------------------------------------------------------- /images/google-car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/google-car.jpg -------------------------------------------------------------------------------- /images/google_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/google_logo.png -------------------------------------------------------------------------------- /images/img_0012.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/img_0012.gif -------------------------------------------------------------------------------- /images/landscape-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/landscape-test.jpg -------------------------------------------------------------------------------- /images/largeref.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/largeref.jpg -------------------------------------------------------------------------------- /images/larrypage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/larrypage.png -------------------------------------------------------------------------------- /images/letter_p.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/letter_p.pdf -------------------------------------------------------------------------------- /images/lorem.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/lorem.pdf -------------------------------------------------------------------------------- /images/malformatted.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/malformatted.pdf -------------------------------------------------------------------------------- /images/mandlebulb_3d_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/mandlebulb_3d_test.png -------------------------------------------------------------------------------- /images/marbles.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/marbles.tif -------------------------------------------------------------------------------- /images/margin-test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/margin-test.pdf -------------------------------------------------------------------------------- /images/max_test_big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/max_test_big.jpg -------------------------------------------------------------------------------- /images/multitarget5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/multitarget5.jpg -------------------------------------------------------------------------------- /images/noise.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/noise.pdf -------------------------------------------------------------------------------- /images/pickrpt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/pickrpt.pdf -------------------------------------------------------------------------------- /images/poster.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/poster.gif -------------------------------------------------------------------------------- /images/printtest.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/printtest.pdf -------------------------------------------------------------------------------- /images/printtest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/printtest.png -------------------------------------------------------------------------------- /images/printtest2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/printtest2.png -------------------------------------------------------------------------------- /images/rosemary.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/rosemary.pdf -------------------------------------------------------------------------------- /images/stepchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/stepchart.jpg -------------------------------------------------------------------------------- /images/testpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/testpage.png -------------------------------------------------------------------------------- /images/testprint.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/testprint.jpeg -------------------------------------------------------------------------------- /images/ticket.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/ticket.pdf -------------------------------------------------------------------------------- /images/version4pdf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/cloudprint_logocert/b0381717a18b96e48d503f218226ac47c8d0f940/images/version4pdf.pdf -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | c199006ea578c81e4bb88ecd1e2e73bfe0b817a6 2 | --------------------------------------------------------------------------------