diff options
| author | Ulf Lilleengen <[email protected]> | 2024-09-04 12:58:33 +0200 |
|---|---|---|
| committer | Ulf Lilleengen <[email protected]> | 2024-09-04 12:58:33 +0200 |
| commit | b76b7ca9f5d64e83f7f69a6f7d97ba605ab2af7f (patch) | |
| tree | 9577beb7c10d467a92d06ac7e96c662c84f74e39 | |
| parent | aabdd45424ae71550be542a3a62872b33a4e0717 (diff) | |
Use at-commands crate and support DNS
* Use at-commands for building and parsing AT commands which has better
error handling.
* Retrieve DNS servers
* Retrieve gateway
* Update example to configure embassy-net with retrieved parameters.
| -rw-r--r-- | embassy-net-nrf91/Cargo.toml | 1 | ||||
| -rw-r--r-- | embassy-net-nrf91/src/context.rs | 208 | ||||
| -rw-r--r-- | examples/nrf9160/Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/nrf9160/src/bin/modem_tcp_client.rs | 78 |
4 files changed, 129 insertions, 159 deletions
diff --git a/embassy-net-nrf91/Cargo.toml b/embassy-net-nrf91/Cargo.toml index e2d596d59..07a0c8886 100644 --- a/embassy-net-nrf91/Cargo.toml +++ b/embassy-net-nrf91/Cargo.toml | |||
| @@ -26,6 +26,7 @@ embassy-net-driver-channel = { version = "0.3.0", path = "../embassy-net-driver- | |||
| 26 | 26 | ||
| 27 | heapless = "0.8" | 27 | heapless = "0.8" |
| 28 | embedded-io = "0.6.1" | 28 | embedded-io = "0.6.1" |
| 29 | at-commands = "0.5.4" | ||
| 29 | 30 | ||
| 30 | [package.metadata.embassy_docs] | 31 | [package.metadata.embassy_docs] |
| 31 | src_base = "https://github.com/embassy-rs/embassy/blob/embassy-net-nrf91-v$VERSION/embassy-net-nrf91/src/" | 32 | src_base = "https://github.com/embassy-rs/embassy/blob/embassy-net-nrf91-v$VERSION/embassy-net-nrf91/src/" |
diff --git a/embassy-net-nrf91/src/context.rs b/embassy-net-nrf91/src/context.rs index c47b1ade6..b532dca14 100644 --- a/embassy-net-nrf91/src/context.rs +++ b/embassy-net-nrf91/src/context.rs | |||
| @@ -2,6 +2,8 @@ use core::net::IpAddr; | |||
| 2 | use heapless::String; | 2 | use heapless::String; |
| 3 | use core::str::FromStr; | 3 | use core::str::FromStr; |
| 4 | use core::fmt::Write; | 4 | use core::fmt::Write; |
| 5 | use heapless::Vec; | ||
| 6 | use at_commands::{builder::CommandBuilder, parser::CommandParser}; | ||
| 5 | 7 | ||
| 6 | /// Provides a higher level API for configuring and reading information for a given | 8 | /// Provides a higher level API for configuring and reading information for a given |
| 7 | /// context id. | 9 | /// context id. |
| @@ -28,10 +30,17 @@ pub enum AuthProt { | |||
| 28 | pub enum Error { | 30 | pub enum Error { |
| 29 | BufferTooSmall, | 31 | BufferTooSmall, |
| 30 | AtCommand, | 32 | AtCommand, |
| 33 | AtParseError, | ||
| 31 | AddrParseError, | 34 | AddrParseError, |
| 32 | Format, | 35 | Format, |
| 33 | } | 36 | } |
| 34 | 37 | ||
| 38 | impl From<at_commands::parser::ParseError> for Error { | ||
| 39 | fn from(_: at_commands::parser::ParseError) -> Self { | ||
| 40 | Self::AtParseError | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 35 | impl From<core::fmt::Error> for Error { | 44 | impl From<core::fmt::Error> for Error { |
| 36 | fn from(_: core::fmt::Error) -> Self { | 45 | fn from(_: core::fmt::Error) -> Self { |
| 37 | Self::Format | 46 | Self::Format |
| @@ -42,6 +51,8 @@ impl From<core::fmt::Error> for Error { | |||
| 42 | pub struct Status { | 51 | pub struct Status { |
| 43 | pub attached: bool, | 52 | pub attached: bool, |
| 44 | pub ip: Option<IpAddr>, | 53 | pub ip: Option<IpAddr>, |
| 54 | pub gateway: Option<IpAddr>, | ||
| 55 | pub dns: Vec<IpAddr, 2>, | ||
| 45 | } | 56 | } |
| 46 | 57 | ||
| 47 | #[cfg(feature = "defmt")] | 58 | #[cfg(feature = "defmt")] |
| @@ -67,129 +78,122 @@ impl<'a> Control<'a> { | |||
| 67 | 78 | ||
| 68 | /// Configures the modem with the provided config. | 79 | /// Configures the modem with the provided config. |
| 69 | pub async fn configure(&self, config: Config<'_>) -> Result<(), Error> { | 80 | pub async fn configure(&self, config: Config<'_>) -> Result<(), Error> { |
| 70 | let mut cmd: String<128> = String::new(); | 81 | let mut cmd: [u8; 256] = [0; 256]; |
| 71 | let mut buf: [u8; 256] = [0; 256]; | 82 | let mut buf: [u8; 256] = [0; 256]; |
| 72 | 83 | ||
| 73 | write!(cmd, "AT+CGDCONT={},\"IP\",\"{}\"", self.cid, config.gateway).map_err(|_| Error::BufferTooSmall)?; | 84 | let op = CommandBuilder::create_set(&mut cmd, true) |
| 74 | let n = self.control.at_command(cmd.as_bytes(), &mut buf).await; | 85 | .named("+CGDCONT") |
| 75 | let mut res = &buf[..n]; | 86 | .with_int_parameter(self.cid) |
| 76 | let res = split_field(&mut res); | 87 | .with_string_parameter("IP") |
| 77 | if res != b"OK" { | 88 | .with_string_parameter(config.gateway) |
| 78 | return Err(Error::AtCommand) | 89 | .finish().map_err(|_| Error::BufferTooSmall)?; |
| 79 | } | 90 | let n = self.control.at_command(op, &mut buf).await; |
| 80 | cmd.clear(); | 91 | CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; |
| 81 | 92 | ||
| 82 | write!(cmd, "AT+CGAUTH={},{}", self.cid, config.auth_prot as u8)?; | 93 | let mut op = CommandBuilder::create_set(&mut cmd, true) |
| 94 | .named("+CGAUTH") | ||
| 95 | .with_int_parameter(self.cid) | ||
| 96 | .with_int_parameter(config.auth_prot as u8); | ||
| 83 | if let Some((username, password)) = config.auth { | 97 | if let Some((username, password)) = config.auth { |
| 84 | write!(cmd, ",\"{}\",\"{}\"", username, password).map_err(|_| Error::BufferTooSmall)?; | 98 | op = op.with_string_parameter(username).with_string_parameter(password); |
| 85 | } | ||
| 86 | let n = self.control.at_command(cmd.as_bytes(), &mut buf).await; | ||
| 87 | let mut res = &buf[..n]; | ||
| 88 | let res = split_field(&mut res); | ||
| 89 | if res != b"OK" { | ||
| 90 | return Err(Error::AtCommand) | ||
| 91 | } | 99 | } |
| 92 | cmd.clear(); | 100 | let op = op.finish().map_err(|_| Error::BufferTooSmall)?; |
| 93 | 101 | ||
| 94 | let n = self.control.at_command(b"AT+CFUN=1", &mut buf).await; | 102 | let n = self.control.at_command(op, &mut buf).await; |
| 95 | let mut res = &buf[..n]; | 103 | CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; |
| 96 | let res = split_field(&mut res); | 104 | |
| 97 | if res != b"OK" { | 105 | let op = CommandBuilder::create_set(&mut cmd, true) |
| 98 | return Err(Error::AtCommand); | 106 | .named("+CFUN") |
| 99 | } | 107 | .with_int_parameter(1) |
| 108 | .finish().map_err(|_| Error::BufferTooSmall)?; | ||
| 109 | let n = self.control.at_command(op, &mut buf).await; | ||
| 110 | CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; | ||
| 100 | 111 | ||
| 101 | Ok(()) | 112 | Ok(()) |
| 102 | } | 113 | } |
| 103 | 114 | ||
| 104 | pub async fn status(&self) -> Result<Status, Error> { | 115 | pub async fn status(&self) -> Result<Status, Error> { |
| 116 | let mut cmd: [u8; 256] = [0; 256]; | ||
| 105 | let mut buf: [u8; 256] = [0; 256]; | 117 | let mut buf: [u8; 256] = [0; 256]; |
| 106 | let n = self.control.at_command(b"AT+CGATT?", &mut buf).await; | ||
| 107 | let mut res = &buf[..n]; | ||
| 108 | pop_prefix(&mut res, b"+CGATT: "); | ||
| 109 | let res = split_field(&mut res); | ||
| 110 | let attached = res == b"1"; | ||
| 111 | 118 | ||
| 119 | let op = CommandBuilder::create_query(&mut cmd, true) | ||
| 120 | .named("+CGATT") | ||
| 121 | .finish().map_err(|_| Error::BufferTooSmall)?; | ||
| 122 | let n = self.control.at_command(op, &mut buf).await; | ||
| 123 | let (res, ) = CommandParser::parse(&buf[..n]) | ||
| 124 | .expect_identifier(b"+CGATT: ") | ||
| 125 | .expect_int_parameter() | ||
| 126 | .expect_identifier(b"\r\nOK").finish()?; | ||
| 127 | let attached = res == 1; | ||
| 112 | if !attached { | 128 | if !attached { |
| 113 | return Ok(Status { attached, ip: None }) | 129 | return Ok(Status { attached, ip: None, gateway: None, dns: Vec::new() }) |
| 114 | } | 130 | } |
| 115 | 131 | ||
| 116 | let mut s: String<128> = String::new(); | 132 | let op = CommandBuilder::create_set(&mut cmd, true) |
| 117 | write!(s, "AT+CGPADDR={}", self.cid)?; | 133 | .named("+CGPADDR") |
| 118 | let n = self.control.at_command(s.as_bytes(), &mut buf).await; | 134 | .with_int_parameter(self.cid) |
| 119 | let mut res = &buf[..n]; | 135 | .finish().map_err(|_| Error::BufferTooSmall)?; |
| 120 | s.clear(); | 136 | let n = self.control.at_command(op, &mut buf).await; |
| 121 | 137 | let (_, ip1, ip2, ) = CommandParser::parse(&buf[..n]) | |
| 122 | write!(s, "+CGPADDR: {},", self.cid)?; | 138 | .expect_identifier(b"+CGPADDR: ") |
| 123 | 139 | .expect_int_parameter() | |
| 124 | if s.len() > res.len() { | 140 | .expect_optional_string_parameter() |
| 125 | let res = split_field(&mut res); | 141 | .expect_optional_string_parameter() |
| 126 | if res == b"OK" { | 142 | .expect_identifier(b"\r\nOK").finish()?; |
| 127 | Ok(Status { attached, ip: None }) | 143 | |
| 144 | let ip = if let Some(ip) = ip1 { | ||
| 145 | let ip = IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?; | ||
| 146 | self.control.open_raw_socket().await; | ||
| 147 | Some(ip) | ||
| 148 | } else { | ||
| 149 | None | ||
| 150 | }; | ||
| 151 | |||
| 152 | let op = CommandBuilder::create_set(&mut cmd, true) | ||
| 153 | .named("+CGCONTRDP") | ||
| 154 | .with_int_parameter(self.cid) | ||
| 155 | .finish().map_err(|_| Error::BufferTooSmall)?; | ||
| 156 | let n = self.control.at_command(op, &mut buf).await; | ||
| 157 | let (_cid, _bid, _apn, _mask, gateway, dns1, dns2, _, _, _, _, mtu) = CommandParser::parse(&buf[..n]) | ||
| 158 | .expect_identifier(b"+CGCONTRDP: ") | ||
| 159 | .expect_int_parameter() | ||
| 160 | .expect_optional_int_parameter() | ||
| 161 | .expect_optional_string_parameter() | ||
| 162 | .expect_optional_string_parameter() | ||
| 163 | .expect_optional_string_parameter() | ||
| 164 | .expect_optional_string_parameter() | ||
| 165 | .expect_optional_string_parameter() | ||
| 166 | .expect_optional_int_parameter() | ||
| 167 | .expect_optional_int_parameter() | ||
| 168 | .expect_optional_int_parameter() | ||
| 169 | .expect_optional_int_parameter() | ||
| 170 | .expect_optional_int_parameter() | ||
| 171 | .expect_identifier(b"\r\nOK").finish()?; | ||
| 172 | |||
| 173 | let gateway = if let Some(ip) = gateway { | ||
| 174 | if ip.is_empty() { | ||
| 175 | None | ||
| 128 | } else { | 176 | } else { |
| 129 | Err(Error::AtCommand) | 177 | Some(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?) |
| 130 | } | 178 | } |
| 131 | } else { | 179 | } else { |
| 132 | pop_prefix(&mut res, s.as_bytes()); | 180 | None |
| 181 | }; | ||
| 133 | 182 | ||
| 134 | let ip = split_field(&mut res); | 183 | let mut dns = Vec::new(); |
| 135 | if !ip.is_empty() { | 184 | if let Some(ip) = dns1 { |
| 136 | let ip = IpAddr::from_str(unsafe { core::str::from_utf8_unchecked(ip) }).map_err(|_| Error::AddrParseError)?; | 185 | dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?).unwrap(); |
| 137 | self.control.open_raw_socket().await; | ||
| 138 | Ok(Status { attached, ip: Some(ip) }) | ||
| 139 | } else { | ||
| 140 | Ok(Status { attached, ip: None }) | ||
| 141 | } | ||
| 142 | } | 186 | } |
| 143 | } | ||
| 144 | } | ||
| 145 | 187 | ||
| 146 | pub(crate) fn is_whitespace(char: u8) -> bool { | 188 | if let Some(ip) = dns2 { |
| 147 | match char { | 189 | dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?).unwrap(); |
| 148 | b'\r' | b'\n' | b' ' => true, | ||
| 149 | _ => false, | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | pub(crate) fn is_separator(char: u8) -> bool { | ||
| 154 | match char { | ||
| 155 | b',' | b'\r' | b'\n' | b' ' => true, | ||
| 156 | _ => false, | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | pub(crate) fn split_field<'a>(data: &mut &'a [u8]) -> &'a [u8] { | ||
| 161 | while !data.is_empty() && is_whitespace(data[0]) { | ||
| 162 | *data = &data[1..]; | ||
| 163 | } | ||
| 164 | |||
| 165 | if data.is_empty() { | ||
| 166 | return &[]; | ||
| 167 | } | ||
| 168 | |||
| 169 | if data[0] == b'"' { | ||
| 170 | let data2 = &data[1..]; | ||
| 171 | let end = data2.iter().position(|&x| x == b'"').unwrap_or(data2.len()); | ||
| 172 | let field = &data2[..end]; | ||
| 173 | let mut rest = &data2[data2.len().min(end + 1)..]; | ||
| 174 | if rest.first() == Some(&b'\"') { | ||
| 175 | rest = &rest[1..]; | ||
| 176 | } | ||
| 177 | while !rest.is_empty() && is_separator(rest[0]) { | ||
| 178 | rest = &rest[1..]; | ||
| 179 | } | 190 | } |
| 180 | *data = rest; | ||
| 181 | field | ||
| 182 | } else { | ||
| 183 | let end = data.iter().position(|&x| is_separator(x)).unwrap_or(data.len()); | ||
| 184 | let field = &data[0..end]; | ||
| 185 | let rest = &data[data.len().min(end + 1)..]; | ||
| 186 | *data = rest; | ||
| 187 | field | ||
| 188 | } | ||
| 189 | } | ||
| 190 | 191 | ||
| 191 | pub(crate) fn pop_prefix(data: &mut &[u8], prefix: &[u8]) { | 192 | Ok(Status { |
| 192 | assert!(data.len() >= prefix.len()); | 193 | attached, |
| 193 | assert!(&data[..prefix.len()] == prefix); | 194 | ip, |
| 194 | *data = &data[prefix.len()..]; | 195 | gateway, |
| 196 | dns, | ||
| 197 | }) | ||
| 198 | } | ||
| 195 | } | 199 | } |
diff --git a/examples/nrf9160/Cargo.toml b/examples/nrf9160/Cargo.toml index fc24e99d2..9aeb99317 100644 --- a/examples/nrf9160/Cargo.toml +++ b/examples/nrf9160/Cargo.toml | |||
| @@ -14,6 +14,7 @@ embassy-net = { version = "0.4.0", path = "../../embassy-net", features = ["defm | |||
| 14 | defmt = "0.3" | 14 | defmt = "0.3" |
| 15 | defmt-rtt = "0.4" | 15 | defmt-rtt = "0.4" |
| 16 | 16 | ||
| 17 | heapless = "0.8" | ||
| 17 | cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } | 18 | cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } |
| 18 | cortex-m-rt = "0.7.0" | 19 | cortex-m-rt = "0.7.0" |
| 19 | panic-probe = { version = "0.3", features = ["print-defmt"] } | 20 | panic-probe = { version = "0.3", features = ["print-defmt"] } |
diff --git a/examples/nrf9160/src/bin/modem_tcp_client.rs b/examples/nrf9160/src/bin/modem_tcp_client.rs index 817ad17c7..f80861693 100644 --- a/examples/nrf9160/src/bin/modem_tcp_client.rs +++ b/examples/nrf9160/src/bin/modem_tcp_client.rs | |||
| @@ -5,9 +5,10 @@ use core::mem::MaybeUninit; | |||
| 5 | use core::net::IpAddr; | 5 | use core::net::IpAddr; |
| 6 | use core::ptr::addr_of_mut; | 6 | use core::ptr::addr_of_mut; |
| 7 | use core::str::FromStr; | 7 | use core::str::FromStr; |
| 8 | use core::{slice, str}; | 8 | use core::slice; |
| 9 | 9 | ||
| 10 | use defmt::{assert, info, warn, unwrap}; | 10 | use defmt::{assert, info, warn, unwrap}; |
| 11 | use heapless::Vec; | ||
| 11 | use embassy_executor::Spawner; | 12 | use embassy_executor::Spawner; |
| 12 | use embassy_net::{Ipv4Address, Ipv4Cidr, Stack, StackResources}; | 13 | use embassy_net::{Ipv4Address, Ipv4Cidr, Stack, StackResources}; |
| 13 | use embassy_net_nrf91::{Runner, State, context}; | 14 | use embassy_net_nrf91::{Runner, State, context}; |
| @@ -146,10 +147,23 @@ async fn main(spawner: Spawner) { | |||
| 146 | }; | 147 | }; |
| 147 | let addr = Ipv4Address(addr.octets()); | 148 | let addr = Ipv4Address(addr.octets()); |
| 148 | 149 | ||
| 150 | let gateway = if let Some(IpAddr::V4(addr)) = status.gateway { | ||
| 151 | Some(Ipv4Address(addr.octets())) | ||
| 152 | } else { | ||
| 153 | None | ||
| 154 | }; | ||
| 155 | |||
| 156 | let mut dns_servers = Vec::new(); | ||
| 157 | for dns in status.dns { | ||
| 158 | if let IpAddr::V4(ip) = dns { | ||
| 159 | unwrap!(dns_servers.push(Ipv4Address(ip.octets()))); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 149 | stack.set_config_v4(embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { | 163 | stack.set_config_v4(embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { |
| 150 | address: Ipv4Cidr::new(addr, 32), | 164 | address: Ipv4Cidr::new(addr, 32), |
| 151 | gateway: None, | 165 | gateway, |
| 152 | dns_servers: Default::default(), | 166 | dns_servers, |
| 153 | })); | 167 | })); |
| 154 | 168 | ||
| 155 | let mut rx_buffer = [0; 4096]; | 169 | let mut rx_buffer = [0; 4096]; |
| @@ -159,15 +173,16 @@ async fn main(spawner: Spawner) { | |||
| 159 | socket.set_timeout(Some(Duration::from_secs(10))); | 173 | socket.set_timeout(Some(Duration::from_secs(10))); |
| 160 | 174 | ||
| 161 | info!("Connecting..."); | 175 | info!("Connecting..."); |
| 162 | let host_addr = embassy_net::Ipv4Address::from_str("83.51.182.206").unwrap(); | 176 | let host_addr = embassy_net::Ipv4Address::from_str("45.79.112.203").unwrap(); |
| 163 | if let Err(e) = socket.connect((host_addr, 8000)).await { | 177 | if let Err(e) = socket.connect((host_addr, 4242)).await { |
| 164 | warn!("connect error: {:?}", e); | 178 | warn!("connect error: {:?}", e); |
| 179 | Timer::after_secs(1).await; | ||
| 165 | continue; | 180 | continue; |
| 166 | } | 181 | } |
| 167 | info!("Connected to {:?}", socket.remote_endpoint()); | 182 | info!("Connected to {:?}", socket.remote_endpoint()); |
| 168 | 183 | ||
| 169 | let msg = b"Hello world!\n"; | 184 | let msg = b"Hello world!\n"; |
| 170 | loop { | 185 | for _ in 0..10 { |
| 171 | if let Err(e) = socket.write_all(msg).await { | 186 | if let Err(e) = socket.write_all(msg).await { |
| 172 | warn!("write error: {:?}", e); | 187 | warn!("write error: {:?}", e); |
| 173 | break; | 188 | break; |
| @@ -177,54 +192,3 @@ async fn main(spawner: Spawner) { | |||
| 177 | } | 192 | } |
| 178 | } | 193 | } |
| 179 | } | 194 | } |
| 180 | |||
| 181 | fn is_whitespace(char: u8) -> bool { | ||
| 182 | match char { | ||
| 183 | b'\r' | b'\n' | b' ' => true, | ||
| 184 | _ => false, | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | fn is_separator(char: u8) -> bool { | ||
| 189 | match char { | ||
| 190 | b',' | b'\r' | b'\n' | b' ' => true, | ||
| 191 | _ => false, | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | fn split_field<'a>(data: &mut &'a [u8]) -> &'a [u8] { | ||
| 196 | while !data.is_empty() && is_whitespace(data[0]) { | ||
| 197 | *data = &data[1..]; | ||
| 198 | } | ||
| 199 | |||
| 200 | if data.is_empty() { | ||
| 201 | return &[]; | ||
| 202 | } | ||
| 203 | |||
| 204 | if data[0] == b'"' { | ||
| 205 | let data2 = &data[1..]; | ||
| 206 | let end = data2.iter().position(|&x| x == b'"').unwrap_or(data2.len()); | ||
| 207 | let field = &data2[..end]; | ||
| 208 | let mut rest = &data2[data2.len().min(end + 1)..]; | ||
| 209 | if rest.first() == Some(&b'\"') { | ||
| 210 | rest = &rest[1..]; | ||
| 211 | } | ||
| 212 | while !rest.is_empty() && is_separator(rest[0]) { | ||
| 213 | rest = &rest[1..]; | ||
| 214 | } | ||
| 215 | *data = rest; | ||
| 216 | field | ||
| 217 | } else { | ||
| 218 | let end = data.iter().position(|&x| is_separator(x)).unwrap_or(data.len()); | ||
| 219 | let field = &data[0..end]; | ||
| 220 | let rest = &data[data.len().min(end + 1)..]; | ||
| 221 | *data = rest; | ||
| 222 | field | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | fn pop_prefix(data: &mut &[u8], prefix: &[u8]) { | ||
| 227 | assert!(data.len() >= prefix.len()); | ||
| 228 | assert!(&data[..prefix.len()] == prefix); | ||
| 229 | *data = &data[prefix.len()..]; | ||
| 230 | } | ||
