diff --git a/mobly/controllers/android_device_lib/adb.py b/mobly/controllers/android_device_lib/adb.py
index 5e0c36f..15e2a5c 100644
--- a/mobly/controllers/android_device_lib/adb.py
+++ b/mobly/controllers/android_device_lib/adb.py
@@ -49,7 +49,7 @@ class AdbError(Error):
             device.
     """
 
-    def __init__(self, cmd, stdout, stderr, ret_code, serial=None):
+    def __init__(self, cmd, stdout, stderr, ret_code, serial=''):
         self.cmd = cmd
         self.stdout = stdout
         self.stderr = stderr
@@ -68,11 +68,15 @@ class AdbTimeoutError(Error):
     Args:
         cmd: list of strings, the adb command that timed out
         timeout: float, the number of seconds passed before timing out.
+        serial: string, the serial of the device the command is executed on.
+            This is an empty string if the adb command is not specific to a
+            device.
     """
 
-    def __init__(self, cmd, timeout):
+    def __init__(self, cmd, timeout, serial=''):
         self.cmd = cmd
         self.timeout = timeout
+        self.serial = serial
 
     def __str__(self):
         return 'Timed out executing command "%s" after %ss.' % (
@@ -159,6 +163,7 @@ class AdbProxy(object):
             The output of the adb command run if exit code is 0.
 
         Raises:
+            ValueError: timeout value is invalid.
             AdbError: The adb command exit code is not 0.
             AdbTimeoutError: The adb command timed out.
         """
@@ -166,13 +171,14 @@ class AdbProxy(object):
             args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
         process = psutil.Process(proc.pid)
         if timeout and timeout <= 0:
-            raise Error('Timeout is not a positive value: %s' % timeout)
+            raise ValueError('Timeout is not a positive value: %s' % timeout)
         if timeout and timeout > 0:
             try:
                 process.wait(timeout=timeout)
             except psutil.TimeoutExpired:
                 process.terminate()
-                raise AdbTimeoutError(cmd=args, timeout=timeout)
+                raise AdbTimeoutError(
+                    cmd=args, timeout=timeout, serial=self.serial)
 
         (out, err) = proc.communicate()
         if stderr:

`AdbError` should have a serial number attr
Our `AdbError` does not include an easily accessible serial number.

When handling an `AdbError`, it is sometimes useful to know which device the command was issued to, especially when you cannot change the code around the adb call directly and have to handle at an upper level.

Is this something worth adding?
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
index 63ae285..9430795 100755
--- a/tests/mobly/controllers/android_device_lib/adb_test.py
+++ b/tests/mobly/controllers/android_device_lib/adb_test.py
@@ -98,9 +98,9 @@ class AdbTest(unittest.TestCase):
         mock_serial = 'ABCD1234'
         with self.assertRaisesRegex(adb.AdbError,
                                     'Error executing adb cmd .*') as context:
-            adb.AdbProxy(mock_serial)._exec_cmd(
-                ['fake_cmd'], shell=False, timeout=None, stderr=None)
+            adb.AdbProxy(mock_serial).fake_cmd()
         self.assertEqual(context.exception.serial, mock_serial)
+        self.assertIn(mock_serial, context.exception.cmd)
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
@@ -129,23 +129,34 @@ class AdbTest(unittest.TestCase):
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
     def test_exec_cmd_timed_out(self, mock_psutil_process, mock_popen):
         self._mock_process(mock_psutil_process, mock_popen)
-        # mock process.wait(timeout=timeout) to
-        # throw psutil.TimeoutExpired exception
         mock_psutil_process.return_value.wait.side_effect = (
             adb.psutil.TimeoutExpired('Timed out'))
+        mock_serial = '1234Abcd'
+        with self.assertRaisesRegex(
+                adb.AdbTimeoutError, 'Timed out executing command "adb -s '
+                '1234Abcd fake-cmd" after 0.01s.') as context:
+            adb.AdbProxy(mock_serial).fake_cmd(timeout=0.01)
+        self.assertEqual(context.exception.serial, mock_serial)
+        self.assertIn(mock_serial, context.exception.cmd)
 
+    @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
+    @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
+    def test_exec_cmd_timed_out_without_serial(self, mock_psutil_process,
+                                               mock_popen):
+        self._mock_process(mock_psutil_process, mock_popen)
+        mock_psutil_process.return_value.wait.side_effect = (
+            adb.psutil.TimeoutExpired('Timed out'))
         with self.assertRaisesRegex(adb.AdbTimeoutError,
-                                    'Timed out executing command "fake_cmd" '
-                                    'after 0.1s.'):
-            adb.AdbProxy()._exec_cmd(
-                ['fake_cmd'], shell=False, timeout=0.1, stderr=None)
+                                    'Timed out executing command "adb '
+                                    'fake-cmd" after 0.01s.') as context:
+            adb.AdbProxy().fake_cmd(timeout=0.01)
 
     @mock.patch('mobly.controllers.android_device_lib.adb.subprocess.Popen')
     @mock.patch('mobly.controllers.android_device_lib.adb.psutil.Process')
     def test_exec_cmd_with_negative_timeout_value(self, mock_psutil_process,
                                                   mock_popen):
         self._mock_process(mock_psutil_process, mock_popen)
-        with self.assertRaisesRegex(adb.Error,
+        with self.assertRaisesRegex(ValueError,
                                     'Timeout is not a positive value: -1'):
             adb.AdbProxy()._exec_cmd(
                 ['fake_cmd'], shell=False, timeout=-1, stderr=None)

pip install -e .[dev]
parse_log_pytest
pytest
pytest mock pytz
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
future==1.0.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
-e git+https://github.com/google/mobly.git@95286a01a566e056d44acfa9577a45bc7f37f51d#egg=mobly
mock==5.2.0
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
portpicker==1.6.0
psutil==7.0.0
pyserial==3.5
pytest @ file:///croot/pytest_1738938843180/work
pytz==2025.2
PyYAML==6.0.2
timeout-decorator==0.5.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: mobly
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - future==1.0.0
      - mock==5.2.0
      - portpicker==1.6.0
      - psutil==7.0.0
      - pyserial==3.5
      - pytz==2025.2
      - pyyaml==6.0.2
      - timeout-decorator==0.5.0
prefix: /opt/conda/envs/mobly

tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_timed_out tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_with_negative_timeout_value
tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_cli_cmd_to_string tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_one_arg_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_one_arg_command_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_one_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_serial tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_serial_with_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_auto_quotes tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_one_arg_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_one_arg_command_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_one_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_serial tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_shell_true_with_serial_with_list tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_construct_adb_cmd_with_special_characters tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_adb_cmd tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_adb_cmd_formats_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_adb_cmd_formats_command_with_shell_true tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_adb_cmd_with_shell_true tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_adb_cmd_with_stderr_pipe tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_error_with_serial tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_error_without_serial tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_no_timeout_success tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_timed_out_without_serial tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_exec_cmd_with_timeout_success tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_adb_and_process_stdout_formats_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_despite_cmd_exits tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_logs_cmd tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_logs_cmd_with_unexpected_stdout tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_raises_adb_error tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_reads_stdout tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_reads_unexpected_stdout tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_returns_stderr tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_when_cmd_eof tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_execute_and_process_stdout_when_handler_crash tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_forward tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_has_shell_command_called_correctly tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_has_shell_command_with_existing_command tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_has_shell_command_with_missing_command_on_newer_devices tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_has_shell_command_with_missing_command_on_older_devices tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_with_handler tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_with_handler_with_options tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_with_handler_with_runner tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_with_options tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_with_runner tests/mobly/controllers/android_device_lib/adb_test.py::AdbTest::test_instrument_without_parameters
mobly/controllers/android_device_lib/adb.py
mobly/controllers/android_device_lib/adb.py