Rust library cleanup

* Fix error code extraction from Python to Rust
* Add documentation for dealing with HCI packets
diff --git a/rust/README.md b/rust/README.md
index e08ef25..17a10f4 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -69,3 +69,68 @@
 ```
 PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features dev-tools
 ```
+
+## HCI packets
+
+Sending a command packet from a device is composed to of two major steps.
+There are more generalized ways of dealing with packets in other scenarios.
+
+### Construct the command
+Pick a command from `src/internal/hci/packets.pdl` and construct its associated "builder" struct.
+
+```rust
+// The "LE Set Scan Enable" command can be found in the Core Bluetooth Spec.
+// It can also be found in `packets.pdl` as `packet LeSetScanEnable : Command`
+fn main() {
+    let device = init_device_as_desired();
+    
+    let le_set_scan_enable_command_builder = LeSetScanEnableBuilder {
+        filter_duplicates: Enable::Disabled,
+        le_scan_enable: Enable::Enabled,
+    };
+}
+```
+
+### Send the command and interpret the event response
+Send the command from an initialized device, and then receive the response.
+
+```rust
+fn main() {
+    // ...
+    
+    // `check_result` to false to receive the event response even if the controller returns a failure code
+    let event = device.send_command(le_set_scan_enable_command_builder.into(), /*check_result*/ false);
+    // Coerce the event into the expected format. A `Command` should have an associated event response
+    // "<command name>Complete".
+    let le_set_scan_enable_complete_event: LeSetScanEnableComplete = event.try_into().unwrap();
+}
+```
+
+### Generic packet handling
+At the very least, you should expect to at least know _which_ kind of base packet you are dealing with. Base packets in 
+`packets.pdl` can be identified because they do not extend any other packet. They are easily found with the regex:
+`^packet [^:]* \{`. For Bluetooth LE (BLE) HCI, one should find some kind of header preceding the packet with the purpose of
+packet disambiguation. We do some of that disambiguation for H4 BLE packets using the `WithPacketHeader` trait at `internal/hci/mod.rs`.
+
+Say you've identified a series of bytes that are certainly an `Acl` packet. They can be parsed using the `Acl` struct.
+```rust
+fn main() {
+    let bytes = bytes_that_are_certainly_acl();
+    let acl_packet = Acl::parse(bytes).unwrap();
+}
+```
+
+Since you don't yet know what kind of `Acl` packet it is, you need to specialize it and then handle the various
+potential cases.
+```rust
+fn main() {
+    // ...
+    match acl_packet.specialize() {
+        Payload(bytes) => do_something(bytes),
+        None => do_something_else(),
+    }
+}
+```
+
+Some packets may yet further embed other packets, in which case you may need to further specialize until no more
+specialization is needed.
diff --git a/rust/examples/broadcast.rs b/rust/examples/broadcast.rs
index affe21e..7b24b76 100644
--- a/rust/examples/broadcast.rs
+++ b/rust/examples/broadcast.rs
@@ -25,7 +25,6 @@
 use pyo3::PyResult;
 use rand::Rng;
 use std::path;
-
 #[pyo3_asyncio::tokio::main]
 async fn main() -> PyResult<()> {
     env_logger::builder()
diff --git a/rust/pytests/wrapper/hci.rs b/rust/pytests/wrapper/hci.rs
index c4ce20d..7512c36 100644
--- a/rust/pytests/wrapper/hci.rs
+++ b/rust/pytests/wrapper/hci.rs
@@ -28,7 +28,7 @@
 };
 use pyo3::{
     exceptions::PyException,
-    {PyErr, PyResult},
+    FromPyObject, IntoPy, Python, {PyErr, PyResult},
 };
 
 #[pyo3_asyncio::tokio::test]
@@ -78,6 +78,28 @@
     Ok(())
 }
 
+#[pyo3_asyncio::tokio::test]
+fn valid_error_code_extraction_succeeds() -> PyResult<()> {
+    let error_code = Python::with_gil(|py| {
+        let python_error_code_success = 0x00_u8.into_py(py);
+        ErrorCode::extract(python_error_code_success.as_ref(py))
+    })?;
+
+    assert_eq!(ErrorCode::Success, error_code);
+    Ok(())
+}
+
+#[pyo3_asyncio::tokio::test]
+fn invalid_error_code_extraction_fails() -> PyResult<()> {
+    let failed_extraction = Python::with_gil(|py| {
+        let python_invalid_error_code = 0xFE_u8.into_py(py);
+        ErrorCode::extract(python_invalid_error_code.as_ref(py))
+    });
+
+    assert!(failed_extraction.is_err());
+    Ok(())
+}
+
 async fn create_local_device(address: Address) -> PyResult<Device> {
     let link = Link::new_local_link()?;
     let controller = Controller::new("C1", None, None, Some(link), Some(address.clone())).await?;
diff --git a/rust/src/wrapper/hci.rs b/rust/src/wrapper/hci.rs
index bf5ffeb..f2ec049 100644
--- a/rust/src/wrapper/hci.rs
+++ b/rust/src/wrapper/hci.rs
@@ -178,7 +178,11 @@
 
 impl<'source> FromPyObject<'source> for ErrorCode {
     fn extract(ob: &'source PyAny) -> PyResult<Self> {
-        ob.extract()
+        // Bumble represents error codes simply as a single-byte number (in Rust, u8)
+        let value: u8 = ob.extract()?;
+        ErrorCode::try_from(value).map_err(|b| {
+            PyErr::new::<PyException, _>(format!("Failed to map {b} to an error code"))
+        })
     }
 }