blob: 1602300f24812490ba65585f6c054120c0243a33 [file] [log] [blame]
Michael Walshed5b46e2017-05-24 11:49:14 -05001#!/usr/bin/env python
2
3r"""
4Provide useful ipmi functions.
5"""
6
Michael Walshf4098fb2018-02-28 10:54:46 -06007import re
Michael Walsh4481b932018-02-08 11:45:15 -06008import gen_print as gp
Michael Walshed5b46e2017-05-24 11:49:14 -05009import gen_misc as gm
Michael Walsh94811f62018-09-05 14:55:12 -050010import gen_cmd as gc
Michael Walshed5b46e2017-05-24 11:49:14 -050011import gen_robot_keyword as grk
12import gen_robot_utils as gru
Michael Walsh4481b932018-02-08 11:45:15 -060013import bmc_ssh_utils as bsu
14import var_funcs as vf
Michael Walsh19e70c82019-01-23 11:07:15 -060015import ipmi_client as ic
Michael Walshed5b46e2017-05-24 11:49:14 -050016import tempfile
17gru.my_import_resource("ipmi_client.robot")
Michael Walsh4481b932018-02-08 11:45:15 -060018from robot.libraries.BuiltIn import BuiltIn
Tony Lee9bca44b2019-12-13 09:34:26 +080019import json
Michael Walshed5b46e2017-05-24 11:49:14 -050020
21
Michael Walshed5b46e2017-05-24 11:49:14 -050022def get_sol_info():
Michael Walshed5b46e2017-05-24 11:49:14 -050023 r"""
24 Get all SOL info and return it as a dictionary.
25
26 Example use:
27
28 Robot code:
29 ${sol_info}= get_sol_info
30 Rpvars sol_info
31
32 Output:
33 sol_info:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -050034 sol_info[Info]: SOL parameter 'Payload Channel (7)'
35 not supported - defaulting to 0x0e
Michael Walshed5b46e2017-05-24 11:49:14 -050036 sol_info[Character Send Threshold]: 1
37 sol_info[Force Authentication]: true
38 sol_info[Privilege Level]: USER
39 sol_info[Set in progress]: set-complete
40 sol_info[Retry Interval (ms)]: 100
41 sol_info[Non-Volatile Bit Rate (kbps)]: IPMI-Over-Serial-Setting
42 sol_info[Character Accumulate Level (ms)]: 100
43 sol_info[Enabled]: true
44 sol_info[Volatile Bit Rate (kbps)]: IPMI-Over-Serial-Setting
45 sol_info[Payload Channel]: 14 (0x0e)
46 sol_info[Payload Port]: 623
47 sol_info[Force Encryption]: true
48 sol_info[Retry Count]: 7
49 """
50
51 status, ret_values = grk.run_key_u("Run IPMI Standard Command sol info")
52
53 # Create temp file path.
54 temp = tempfile.NamedTemporaryFile()
55 temp_file_path = temp.name
56
57 # Write sol info to temp file path.
58 text_file = open(temp_file_path, "w")
59 text_file.write(ret_values)
60 text_file.close()
61
62 # Use my_parm_file to interpret data.
63 sol_info = gm.my_parm_file(temp_file_path)
64
65 return sol_info
66
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050067
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050068def set_sol_setting(setting_name, setting_value):
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050069 r"""
70 Set SOL setting with given value.
71
72 # Description of argument(s):
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -050073 # setting_name SOL setting which needs to be set (e.g.
74 # "retry-count").
75 # setting_value Value which needs to be set (e.g. "7").
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050076 """
77
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -050078 status, ret_values = grk.run_key_u("Run IPMI Standard Command sol set "
79 + setting_name + " " + setting_value)
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050080
81 return status
82
Rahul Maheshwarid629b5c2017-05-23 08:06:28 -050083
Michael Walsh94811f62018-09-05 14:55:12 -050084def execute_ipmi_cmd(cmd_string,
85 ipmi_cmd_type='inband',
86 print_output=1,
Michael Walsh19e70c82019-01-23 11:07:15 -060087 ignore_err=0,
88 **options):
Michael Walsh94811f62018-09-05 14:55:12 -050089 r"""
90 Run the given command string as an IPMI command and return the stdout,
91 stderr and the return code.
92
93 Description of argument(s):
94 cmd_string The command string to be run as an IPMI
95 command.
96 ipmi_cmd_type 'inband' or 'external'.
97 print_output If this is set, this function will print
98 the stdout/stderr generated by
99 the IPMI command.
100 ignore_err Ignore error means that a failure
101 encountered by running the command
102 string will not be raised as a python
103 exception.
Michael Walsh19e70c82019-01-23 11:07:15 -0600104 options These are passed directly to the
105 create_ipmi_ext_command_string function.
106 See that function's prolog for details.
Michael Walsh94811f62018-09-05 14:55:12 -0500107 """
108
109 if ipmi_cmd_type == 'inband':
110 IPMI_INBAND_CMD = BuiltIn().get_variable_value("${IPMI_INBAND_CMD}")
111 cmd_buf = IPMI_INBAND_CMD + " " + cmd_string
112 return bsu.os_execute_command(cmd_buf,
113 print_out=print_output,
114 ignore_err=ignore_err)
115
116 if ipmi_cmd_type == 'external':
Michael Walsh19e70c82019-01-23 11:07:15 -0600117 cmd_buf = ic.create_ipmi_ext_command_string(cmd_string, **options)
Michael Walsh94811f62018-09-05 14:55:12 -0500118 rc, stdout, stderr = gc.shell_cmd(cmd_buf,
119 print_output=print_output,
120 ignore_err=ignore_err,
121 return_stderr=1)
122 return stdout, stderr, rc
123
124
Tony Leeea741302019-11-08 11:01:58 +0800125def get_lan_print_dict(channel_number='', ipmi_cmd_type='external'):
Michael Walsh4481b932018-02-08 11:45:15 -0600126 r"""
127 Get IPMI 'lan print' output and return it as a dictionary.
128
129 Here is an example of the IPMI lan print output:
130
131 Set in Progress : Set Complete
132 Auth Type Support : MD5
133 Auth Type Enable : Callback : MD5
134 : User : MD5
135 : Operator : MD5
136 : Admin : MD5
137 : OEM : MD5
138 IP Address Source : Static Address
139 IP Address : x.x.x.x
140 Subnet Mask : x.x.x.x
141 MAC Address : xx:xx:xx:xx:xx:xx
142 Default Gateway IP : x.x.x.x
143 802.1q VLAN ID : Disabled
144 Cipher Suite Priv Max : Not Available
145 Bad Password Threshold : Not Available
146
147 Given that data, this function will return the following dictionary.
148
149 lan_print_dict:
150 [Set in Progress]: Set Complete
151 [Auth Type Support]: MD5
152 [Auth Type Enable]:
153 [Callback]: MD5
154 [User]: MD5
155 [Operator]: MD5
156 [Admin]: MD5
157 [OEM]: MD5
158 [IP Address Source]: Static Address
159 [IP Address]: x.x.x.x
160 [Subnet Mask]: x.x.x.x
161 [MAC Address]: xx:xx:xx:xx:xx:xx
162 [Default Gateway IP]: x.x.x.x
163 [802.1q VLAN ID]: Disabled
164 [Cipher Suite Priv Max]: Not Available
165 [Bad Password Threshold]: Not Available
166
Michael Walsh94811f62018-09-05 14:55:12 -0500167 Description of argument(s):
168 ipmi_cmd_type The type of ipmi command to use (e.g.
169 'inband', 'external').
Michael Walsh4481b932018-02-08 11:45:15 -0600170 """
171
Tony Leeea741302019-11-08 11:01:58 +0800172 channel_number = str(channel_number)
Michael Walsh4481b932018-02-08 11:45:15 -0600173 # Notice in the example of data above that 'Auth Type Enable' needs some
174 # special processing. We essentially want to isolate its data and remove
175 # the 'Auth Type Enable' string so that key_value_outbuf_to_dict can
176 # process it as a sub-dictionary.
Tony Leeea741302019-11-08 11:01:58 +0800177 cmd_buf = "lan print " + channel_number + " | grep -E '^(Auth Type Enable)" +\
Michael Walsh4481b932018-02-08 11:45:15 -0600178 "?[ ]+: ' | sed -re 's/^(Auth Type Enable)?[ ]+: //g'"
Michael Walsh94811f62018-09-05 14:55:12 -0500179 stdout1, stderr, rc = execute_ipmi_cmd(cmd_buf, ipmi_cmd_type,
180 print_output=0)
Michael Walsh4481b932018-02-08 11:45:15 -0600181
182 # Now get the remainder of the data and exclude the lines with no field
183 # names (i.e. the 'Auth Type Enable' sub-fields).
Tony Leeea741302019-11-08 11:01:58 +0800184 cmd_buf = "lan print " + channel_number + " | grep -E -v '^[ ]+: '"
Michael Walsh94811f62018-09-05 14:55:12 -0500185 stdout2, stderr, rc = execute_ipmi_cmd(cmd_buf, ipmi_cmd_type,
186 print_output=0)
Michael Walsh4481b932018-02-08 11:45:15 -0600187
188 # Make auth_type_enable_dict sub-dictionary...
189 auth_type_enable_dict = vf.key_value_outbuf_to_dict(stdout1, to_lower=0,
190 underscores=0)
191
192 # Create the lan_print_dict...
193 lan_print_dict = vf.key_value_outbuf_to_dict(stdout2, to_lower=0,
194 underscores=0)
195 # Re-assign 'Auth Type Enable' to contain the auth_type_enable_dict.
196 lan_print_dict['Auth Type Enable'] = auth_type_enable_dict
197
198 return lan_print_dict
Michael Walshd59ed7c2018-02-15 10:19:38 -0600199
200
Michael Walshf4098fb2018-02-28 10:54:46 -0600201def get_ipmi_power_reading(strip_watts=1):
Michael Walshd59ed7c2018-02-15 10:19:38 -0600202 r"""
203 Get IPMI power reading data and return it as a dictionary.
204
205 The data is obtained by issuing the IPMI "power reading" command. An
206 example is shown below:
207
208 Instantaneous power reading: 234 Watts
209 Minimum during sampling period: 234 Watts
210 Maximum during sampling period: 234 Watts
211 Average power reading over sample period: 234 Watts
212 IPMI timestamp: Thu Jan 1 00:00:00 1970
213 Sampling period: 00000000 Seconds.
214 Power reading state is: deactivated
215
216 For the data shown above, the following dictionary will be returned.
217
218 result:
219 [instantaneous_power_reading]: 238 Watts
220 [minimum_during_sampling_period]: 238 Watts
221 [maximum_during_sampling_period]: 238 Watts
222 [average_power_reading_over_sample_period]: 238 Watts
223 [ipmi_timestamp]: Thu Jan 1 00:00:00 1970
224 [sampling_period]: 00000000 Seconds.
225 [power_reading_state_is]: deactivated
Michael Walshf4098fb2018-02-28 10:54:46 -0600226
227 Description of argument(s):
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500228 strip_watts Strip all dictionary values of the
229 trailing " Watts" substring.
Michael Walshd59ed7c2018-02-15 10:19:38 -0600230 """
231
232 status, ret_values = \
233 grk.run_key_u("Run IPMI Standard Command dcmi power reading")
234 result = vf.key_value_outbuf_to_dict(ret_values)
235
Michael Walshf4098fb2018-02-28 10:54:46 -0600236 if strip_watts:
237 result.update((k, re.sub(' Watts$', '', v)) for k, v in result.items())
238
Michael Walshd59ed7c2018-02-15 10:19:38 -0600239 return result
Michael Walshaf5607e2018-02-19 17:37:20 -0600240
241
242def get_mc_info():
Michael Walshaf5607e2018-02-19 17:37:20 -0600243 r"""
244 Get IPMI mc info data and return it as a dictionary.
245
246 The data is obtained by issuing the IPMI "mc info" command. An
247 example is shown below:
248
249 Device ID : 0
250 Device Revision : 0
251 Firmware Revision : 2.01
252 IPMI Version : 2.0
253 Manufacturer ID : 42817
254 Manufacturer Name : Unknown (0xA741)
255 Product ID : 16975 (0x424f)
256 Product Name : Unknown (0x424F)
257 Device Available : yes
258 Provides Device SDRs : yes
259 Additional Device Support :
260 Sensor Device
261 SEL Device
262 FRU Inventory Device
263 Chassis Device
264 Aux Firmware Rev Info :
265 0x00
266 0x00
267 0x00
268 0x00
269
270 For the data shown above, the following dictionary will be returned.
271 mc_info:
272 [device_id]: 0
273 [device_revision]: 0
274 [firmware_revision]: 2.01
275 [ipmi_version]: 2.0
276 [manufacturer_id]: 42817
277 [manufacturer_name]: Unknown (0xA741)
278 [product_id]: 16975 (0x424f)
279 [product_name]: Unknown (0x424F)
280 [device_available]: yes
281 [provides_device_sdrs]: yes
282 [additional_device_support]:
283 [additional_device_support][0]: Sensor Device
284 [additional_device_support][1]: SEL Device
285 [additional_device_support][2]: FRU Inventory Device
286 [additional_device_support][3]: Chassis Device
287 [aux_firmware_rev_info]:
288 [aux_firmware_rev_info][0]: 0x00
289 [aux_firmware_rev_info][1]: 0x00
290 [aux_firmware_rev_info][2]: 0x00
291 [aux_firmware_rev_info][3]: 0x00
292 """
293
294 status, ret_values = \
295 grk.run_key_u("Run IPMI Standard Command mc info")
296 result = vf.key_value_outbuf_to_dict(ret_values, process_indent=1)
297
298 return result
Rahul Maheshwaridc6a32c2018-03-15 05:21:55 -0500299
300
301def get_sdr_info():
Rahul Maheshwaridc6a32c2018-03-15 05:21:55 -0500302 r"""
303 Get IPMI sdr info data and return it as a dictionary.
304
305 The data is obtained by issuing the IPMI "sdr info" command. An
306 example is shown below:
307
308 SDR Version : 0x51
309 Record Count : 216
310 Free Space : unspecified
311 Most recent Addition :
312 Most recent Erase :
313 SDR overflow : no
314 SDR Repository Update Support : unspecified
315 Delete SDR supported : no
316 Partial Add SDR supported : no
317 Reserve SDR repository supported : no
318 SDR Repository Alloc info supported : no
319
320 For the data shown above, the following dictionary will be returned.
321 mc_info:
322
323 [sdr_version]: 0x51
324 [record_Count]: 216
325 [free_space]: unspecified
326 [most_recent_addition]:
327 [most_recent_erase]:
328 [sdr_overflow]: no
329 [sdr_repository_update_support]: unspecified
330 [delete_sdr_supported]: no
331 [partial_add_sdr_supported]: no
332 [reserve_sdr_repository_supported]: no
333 [sdr_repository_alloc_info_supported]: no
334 """
335
336 status, ret_values = \
337 grk.run_key_u("Run IPMI Standard Command sdr info")
338 result = vf.key_value_outbuf_to_dict(ret_values, process_indent=1)
339
340 return result
George Keishing3511a3f2018-04-19 10:38:30 -0500341
342
343def get_aux_version(version_id):
344 r"""
345 Get IPMI Aux version info data and return it.
346
347 Description of argument(s):
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500348 version_id The data is obtained by from BMC
349 /etc/os-release
350 (e.g. "xxx-v2.1-438-g0030304-r3-gfea8585").
George Keishing3511a3f2018-04-19 10:38:30 -0500351
352 In the prior example, the 3rd field is "438" is the commit version and
353 the 5th field is "r3" and value "3" is the release version.
354
355 Aux version return from this function 4380003.
356 """
357
358 # Commit version.
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500359 count = re.findall("-(\\d{1,4})-", version_id)
George Keishing3511a3f2018-04-19 10:38:30 -0500360
361 # Release version.
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500362 release = re.findall("-r(\\d{1,4})", version_id)
George Keishing3511a3f2018-04-19 10:38:30 -0500363 if release:
364 aux_version = count[0] + "{0:0>4}".format(release[0])
365 else:
366 aux_version = count[0] + "0000"
367
368 return aux_version
Michael Walsh27b14a62018-05-24 11:05:07 -0500369
370
371def get_fru_info():
372 r"""
373 Get fru info and return it as a list of dictionaries.
374
375 The data is obtained by issuing the IPMI "fru print -N 50" command. An
376 example is shown below:
377
378 FRU Device Description : Builtin FRU Device (ID 0)
379 Device not present (Unspecified error)
380
381 FRU Device Description : cpu0 (ID 1)
382 Board Mfg Date : Sun Dec 31 18:00:00 1995
George Keishinge0a81282018-06-08 10:02:30 -0500383 Board Mfg : <Manufacturer Name>
Michael Walsh27b14a62018-05-24 11:05:07 -0500384 Board Product : PROCESSOR MODULE
385 Board Serial : YA1934315964
386 Board Part Number : 02CY209
387
388 FRU Device Description : cpu1 (ID 2)
389 Board Mfg Date : Sun Dec 31 18:00:00 1995
George Keishinge0a81282018-06-08 10:02:30 -0500390 Board Mfg : <Manufacturer Name>
Michael Walsh27b14a62018-05-24 11:05:07 -0500391 Board Product : PROCESSOR MODULE
392 Board Serial : YA1934315965
393 Board Part Number : 02CY209
394
395 For the data shown above, the following list of dictionaries will be
396 returned.
397
398 fru_obj:
399 fru_obj[0]:
400 [fru_device_description]: Builtin FRU Device (ID 0)
401 [state]: Device not present (Unspecified error)
402 fru_obj[1]:
403 [fru_device_description]: cpu0 (ID 1)
404 [board_mfg_date]: Sun Dec 31 18:00:00 1995
George Keishinge0a81282018-06-08 10:02:30 -0500405 [board_mfg]: <Manufacturer Name>
Michael Walsh27b14a62018-05-24 11:05:07 -0500406 [board_product]: PROCESSOR MODULE
407 [board_serial]: YA1934315964
408 [board_part_number]: 02CY209
409 fru_obj[2]:
410 [fru_device_description]: cpu1 (ID 2)
411 [board_mfg_date]: Sun Dec 31 18:00:00 1995
George Keishinge0a81282018-06-08 10:02:30 -0500412 [board_mfg]: <Manufacturer Name>
Michael Walsh27b14a62018-05-24 11:05:07 -0500413 [board_product]: PROCESSOR MODULE
414 [board_serial]: YA1934315965
415 [board_part_number]: 02CY209
416 """
417
418 status, ret_values = \
419 grk.run_key_u("Run IPMI Standard Command fru print -N 50")
420
421 # Manipulate the "Device not present" line to create a "state" key.
422 ret_values = re.sub("Device not present", "state : Device not present",
423 ret_values)
424
425 return [vf.key_value_outbuf_to_dict(x) for x in re.split("\n\n",
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500426 ret_values)]
Michael Walsh61224e62018-05-30 17:58:42 -0500427
428
Michael Walsha95e4ef2018-06-06 17:53:04 -0500429def get_component_fru_info(component='cpu',
430 fru_objs=None):
Michael Walsh61224e62018-05-30 17:58:42 -0500431 r"""
432 Get fru info for the given component and return it as a list of
433 dictionaries.
434
435 This function calls upon get_fru_info and then filters out the unwanted
436 entries. See get_fru_info's prolog for a layout of the data.
437
438 Description of argument(s):
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500439 component The component (e.g. "cpu", "dimm", etc.).
440 fru_objs A fru_objs list such as the one returned
441 by get_fru_info. If this is None, then
442 this function will call get_fru_info to
443 obtain such a list.
444 Supplying this argument may improve
445 performance if this function is to be
446 called multiple times.
Michael Walsh61224e62018-05-30 17:58:42 -0500447 """
448
Michael Walsha95e4ef2018-06-06 17:53:04 -0500449 if fru_objs is None:
450 fru_objs = get_fru_info()
Michael Walsh61224e62018-05-30 17:58:42 -0500451 return\
452 [x for x in fru_objs
453 if re.match(component + '([0-9]+)? ', x['fru_device_description'])]
Rahul Maheshwari24e0e792019-02-12 22:40:23 -0600454
455
456def get_user_info(userid, channel_number=1):
457 r"""
458 Get user info using channel command and return it as a dictionary.
459
460 Description of argument(s):
461 userid The userid (e.g. "1", "2", etc.).
462 channel_number The user's channel number (e.g. "1").
463
Michael Walshe733b262019-12-09 11:45:24 -0600464 Note: If userid is blank, this function will return a list of dictionaries. Each list entry represents
465 one userid record.
466
Rahul Maheshwari24e0e792019-02-12 22:40:23 -0600467 The data is obtained by issuing the IPMI "channel getaccess" command. An
468 example is shown below for user id 1 and channel number 1.
469
470 Maximum User IDs : 15
471 Enabled User IDs : 1
472 User ID : 1
473 User Name : root
474 Fixed Name : No
475 Access Available : callback
476 Link Authentication : enabled
477 IPMI Messaging : enabled
478 Privilege Level : ADMINISTRATOR
479 Enable Status : enabled
480
481 For the data shown above, the following dictionary will be returned.
482
483 user_info:
484 [maximum_userids]: 15
485 [enabled_userids: 1
486 [userid] 1
487 [user_name] root
488 [fixed_name] No
489 [access_available] callback
490 [link_authentication] enabled
491 [ipmi_messaging] enabled
492 [privilege_level] ADMINISTRATOR
493 [enable_status] enabled
Rahul Maheshwari24e0e792019-02-12 22:40:23 -0600494 """
495
496 status, ret_values = grk.run_key_u("Run IPMI Standard Command channel getaccess "
497 + str(channel_number) + " " + str(userid))
498
Michael Walshe733b262019-12-09 11:45:24 -0600499 if userid == "":
500 return vf.key_value_outbuf_to_dicts(ret_values, process_indent=1)
501 else:
502 return vf.key_value_outbuf_to_dict(ret_values, process_indent=1)
George Keishing45511e82019-10-15 01:26:55 -0500503
504
505def channel_getciphers_ipmi():
506
507 r"""
508 Run 'channel getciphers ipmi' command and return the result as a list of dictionaries.
509
510 Example robot code:
511 ${ipmi_channel_ciphers}= Channel Getciphers IPMI
512 Rprint Vars ipmi_channel_ciphers
513
514 Example output:
515 ipmi_channel_ciphers:
516 [0]:
517 [id]: 3
518 [iana]: N/A
519 [auth_alg]: hmac_sha1
520 [integrity_alg]: hmac_sha1_96
521 [confidentiality_alg]: aes_cbc_128
522 [1]:
523 [id]: 17
524 [iana]: N/A
525 [auth_alg]: hmac_sha256
526 [integrity_alg]: sha256_128
527 [confidentiality_alg]: aes_cbc_128
528 """
529
530 cmd_buf = "channel getciphers ipmi | sed -re 's/ Alg/_Alg/g'"
531 stdout, stderr, rc = execute_ipmi_cmd(cmd_buf, "external", print_output=0)
532 return vf.outbuf_to_report(stdout)
Tony Lee9bca44b2019-12-13 09:34:26 +0800533
534
535def get_device_id_config():
536 r"""
537 Get the device id config data and return as a dictionary.
538
539 Example:
540
541 dev_id_config = get_device_id_config()
542 print_vars(dev_id_config)
543
544 dev_id_config:
545 [manuf_id]: 7244
546 [addn_dev_support]: 141
547 [prod_id]: 16976
548 [aux]: 0
549 [id]: 32
550 [revision]: 129
551 [device_revision]: 1
552 """
553 stdout, stderr, rc = bsu.bmc_execute_command("cat /usr/share/ipmi-providers/dev_id.json")
554
555 result = json.loads(stdout)
556
557 # Create device revision field for the user.
558 # Reference IPMI specification v2.0 "Get Device ID Command"
559 # [7] 1 = device provides Device SDRs
560 # 0 = device does not provide Device SDRs
561 # [6:4] reserved. Return as 0.
562 # [3:0] Device Revision, binary encoded.
563
564 result['device_revision'] = result['revision'] & 0x0F
565
566 return result