use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, ValueEnum};
use sensus_core::{
pipeline::{FilterStep, Pipeline},
stereo::{read_xmp_depth, split_mpo, stereo_to_depth},
vision::{depth_aware_blur, DepthBlurKind},
AudioPipeline, Error as CoreError, Filter as CoreFilter, HearingFilter,
};
use thiserror::Error;
mod audio;
const DEPTH_BLUR_MAX_RADIUS_RATIO: f32 = 0.023;
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Filter {
Protanopia,
Deuteranopia,
Tritanopia,
Achromatopsia,
Tetrachromacy,
Myopia,
Hyperopia,
Astigmatism,
Presbyopia,
Glaucoma,
MacularDegeneration,
Hemianopia,
TunnelVision,
Cataract,
Floaters,
Photophobia,
NightBlindness,
Vertigo,
BppvRotation,
VestibularNeuritis,
Diplopia,
Nystagmus,
Starbursts,
EyeStrain,
DryEye,
Metamorphopsia,
ContrastSensitivity,
DetailLoss,
Teichopsia,
FlickeringStars,
MyopiaDepth,
HyperopiaDepth,
DepthOfField,
}
impl Filter {
fn is_depth_filter(self) -> bool {
matches!(
self,
Filter::MyopiaDepth | Filter::HyperopiaDepth | Filter::DepthOfField
)
}
fn depth_kind(self) -> Option<DepthBlurKind> {
match self {
Filter::MyopiaDepth => Some(DepthBlurKind::Myopia),
Filter::HyperopiaDepth => Some(DepthBlurKind::Hyperopia),
Filter::DepthOfField => Some(DepthBlurKind::DepthOfField),
_ => None,
}
}
fn to_core(self, cli: &Cli) -> CoreFilter {
match self {
Filter::Protanopia => CoreFilter::Protanopia,
Filter::Deuteranopia => CoreFilter::Deuteranopia,
Filter::Tritanopia => CoreFilter::Tritanopia,
Filter::Achromatopsia => CoreFilter::Achromatopsia,
Filter::Tetrachromacy => CoreFilter::Tetrachromacy,
Filter::Myopia => CoreFilter::Myopia,
Filter::Hyperopia => CoreFilter::Hyperopia,
Filter::Astigmatism => CoreFilter::Astigmatism { axis_deg: cli.axis },
Filter::Presbyopia => CoreFilter::Presbyopia,
Filter::Glaucoma => CoreFilter::Glaucoma {
mode: sensus_core::vision::GlaucomaMode::Vignette,
},
Filter::MacularDegeneration => CoreFilter::MacularDegeneration,
Filter::Hemianopia => CoreFilter::Hemianopia { side: cli.side },
Filter::TunnelVision => CoreFilter::TunnelVision,
Filter::Cataract => CoreFilter::Cataract { seed: cli.seed },
Filter::Floaters => CoreFilter::Floaters {
seed: cli.seed,
density: cli.density,
size: cli.size,
gaze_x: cli.gaze_x,
gaze_y: cli.gaze_y,
},
Filter::Photophobia => CoreFilter::Photophobia,
Filter::NightBlindness => CoreFilter::NightBlindness,
Filter::Vertigo => CoreFilter::Vertigo,
Filter::BppvRotation => CoreFilter::BppvRotation,
Filter::VestibularNeuritis => CoreFilter::VestibularNeuritis,
Filter::Diplopia => CoreFilter::Diplopia {
offset_x: cli.offset_x,
offset_y: cli.offset_y,
ghost_strength: cli.ghost_strength,
},
Filter::Nystagmus => CoreFilter::Nystagmus {
amplitude: cli.amplitude,
direction_deg: cli.direction_deg,
},
Filter::Starbursts => CoreFilter::Starbursts {
num_rays: cli.num_rays,
ray_length_ratio: cli.ray_length,
threshold: cli.threshold,
dispersion: cli.dispersion,
},
Filter::EyeStrain => CoreFilter::EyeStrain,
Filter::DryEye => CoreFilter::DryEye,
Filter::Metamorphopsia => CoreFilter::Metamorphopsia {
freq: cli.meta_freq,
seed: cli.meta_seed,
},
Filter::ContrastSensitivity => CoreFilter::ContrastSensitivity,
Filter::DetailLoss => CoreFilter::DetailLoss {
cell_size: cli.cell_size,
},
Filter::Teichopsia => CoreFilter::Teichopsia,
Filter::FlickeringStars => CoreFilter::FlickeringStars { seed: cli.seed },
Filter::MyopiaDepth | Filter::HyperopiaDepth | Filter::DepthOfField => {
unreachable!("depth filters must be handled separately")
}
}
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Hearing {
HearingLoss,
SuddenHearingLoss,
NoiseInducedHearingLoss,
Tinnitus,
Hyperacusis,
Misophonia,
Paracusis,
Amusia,
Dysmelodia,
PitchShift,
Diplacusis,
AuditoryProcessingDisorder,
Meniere,
Labyrinthitis,
}
impl Hearing {
fn to_core(self, cli: &Cli) -> HearingFilter {
match self {
Hearing::HearingLoss => HearingFilter::HearingLoss,
Hearing::SuddenHearingLoss => HearingFilter::SuddenHearingLoss { freq_hz: cli.freq },
Hearing::NoiseInducedHearingLoss => HearingFilter::NoiseInducedHearingLoss,
Hearing::Tinnitus => HearingFilter::Tinnitus { freq_hz: cli.freq },
Hearing::Hyperacusis => HearingFilter::Hyperacusis,
Hearing::Misophonia => HearingFilter::Misophonia { freq_hz: cli.freq },
Hearing::Paracusis => HearingFilter::Paracusis,
Hearing::Amusia => HearingFilter::Amusia,
Hearing::Dysmelodia => HearingFilter::Dysmelodia,
Hearing::PitchShift => HearingFilter::PitchShift {
semitones: cli.semitones,
},
Hearing::Diplacusis => HearingFilter::Diplacusis,
Hearing::AuditoryProcessingDisorder => HearingFilter::AuditoryProcessingDisorder,
Hearing::Meniere => HearingFilter::Meniere,
Hearing::Labyrinthitis => HearingFilter::Labyrinthitis,
}
}
}
#[derive(Debug, Parser)]
#[command(name = "sensus", version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
input: Option<PathBuf>,
#[arg(short, long, required_unless_present = "pipe")]
output: Option<PathBuf>,
#[arg(short, long, value_enum, num_args = 1..)]
filter: Vec<Filter>,
#[arg(short, long, default_value_t = 1.0, value_parser = parse_strength)]
strength: f32,
#[arg(long, default_value_t = 90.0, value_parser = parse_axis)]
axis: f32,
#[arg(long, default_value = "0")]
seed: u64,
#[arg(long, default_value = "0.5")]
density: f32,
#[arg(long, default_value = "0.5")]
gaze_x: f32,
#[arg(long, default_value = "0.5")]
gaze_y: f32,
#[arg(long, default_value = "1.0", value_parser = parse_floater_size)]
size: f32,
#[arg(long, default_value = "0.0")]
side: f32,
#[arg(long)]
depth: Option<PathBuf>,
#[arg(long)]
mpo: Option<PathBuf>,
#[arg(long, conflicts_with = "mpo")]
portrait: Option<PathBuf>,
#[arg(long, default_value = "0.5", value_parser = parse_focus)]
focus: f32,
#[arg(long, default_value = "0.02", value_parser = parse_signed_ratio)]
offset_x: f32,
#[arg(long, default_value = "0.01", value_parser = parse_signed_ratio)]
offset_y: f32,
#[arg(long, default_value = "0.7", value_parser = parse_ratio)]
ghost_strength: f32,
#[arg(long, default_value = "0.03", value_parser = parse_ratio)]
amplitude: f32,
#[arg(long, default_value = "0.0", value_parser = parse_direction_deg)]
direction_deg: f32,
#[arg(long, default_value = "6")]
num_rays: u32,
#[arg(long, default_value = "0.1", value_parser = parse_ratio)]
ray_length: f32,
#[arg(long, default_value = "0.8", value_parser = parse_ratio)]
threshold: f32,
#[arg(long, default_value = "0.0", value_parser = parse_ratio)]
dispersion: f32,
#[arg(long, default_value = "8")]
cell_size: u32,
#[arg(long, default_value = "4.0", value_parser = parse_meta_freq)]
meta_freq: f32,
#[arg(long, default_value = "0")]
meta_seed: u64,
#[arg(long, conflicts_with = "input")]
pipe: bool,
#[arg(long, conflicts_with_all = ["input", "pipe", "mpo", "portrait"])]
audio: Option<PathBuf>,
#[arg(long, value_enum, num_args = 1..)]
hearing: Vec<Hearing>,
#[arg(long, default_value_t = 4000.0, value_parser = parse_freq)]
freq: f32,
#[arg(long, default_value_t = 0.0, value_parser = parse_semitones)]
semitones: f32,
}
fn parse_strength(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(0.0..=1.0).contains(&v) {
return Err(format!("strength must be in 0.0..=1.0, got {v}"));
}
Ok(v)
}
fn parse_ratio(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(0.0..=1.0).contains(&v) {
return Err(format!("value must be in 0.0..=1.0, got {v}"));
}
Ok(v)
}
fn parse_signed_ratio(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(-1.0..=1.0).contains(&v) {
return Err(format!("value must be in -1.0..=1.0, got {v}"));
}
Ok(v)
}
fn parse_floater_size(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if !v.is_finite() || !(0.1..=5.0).contains(&v) {
return Err(format!("size must be in 0.1..=5.0, got {v}"));
}
Ok(v)
}
fn parse_meta_freq(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if !v.is_finite() || v <= 0.0 {
return Err(format!("meta-freq must be finite and > 0.0, got {v}"));
}
Ok(v)
}
fn parse_focus(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(0.0..=1.0).contains(&v) {
return Err(format!("focus must be in 0.0..=1.0, got {v}"));
}
Ok(v)
}
fn parse_axis(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(0.0..=180.0).contains(&v) {
return Err(format!("axis must be in 0.0..=180.0 degrees, got {v}"));
}
Ok(v)
}
fn parse_freq(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if !v.is_finite() || v <= 0.0 || v > 20000.0 {
return Err(format!(
"freq must be in 0.0 (exclusive)..=20000.0 Hz, got {v}"
));
}
Ok(v)
}
fn parse_semitones(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if !v.is_finite() || !(-48.0..=48.0).contains(&v) {
return Err(format!(
"semitones must be finite and in -48.0..=48.0, got {v}"
));
}
Ok(v)
}
fn parse_direction_deg(s: &str) -> Result<f32, String> {
let v: f32 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
if v.is_nan() || !(0.0..=360.0).contains(&v) {
return Err(format!("direction-deg must be in 0.0..=360.0, got {v}"));
}
Ok(v)
}
#[derive(Debug, Error)]
enum RunError {
#[error("sensus: failed to open input {path:?}: {source}")]
InputOpen {
path: PathBuf,
#[source]
source: image::ImageError,
},
#[error("sensus: failed to save output {path:?}: {source}")]
OutputSave {
path: PathBuf,
#[source]
source: image::ImageError,
},
#[error("{0}")]
Pipeline(String),
#[error("sensus: failed to read MPO file {path:?}: {source}")]
MpoRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{0}")]
MpoError(String),
#[error("sensus: failed to read portrait file {path:?}: {source}")]
PortraitRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{0}")]
PortraitError(String),
#[error("{0}")]
InputRequired(String),
#[error("{0}")]
AudioError(String),
#[error("sensus: I/O error: {0}")]
Io(#[from] std::io::Error),
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(RunError::Pipeline(msg)) => {
eprintln!("{msg}");
ExitCode::FAILURE
}
Err(RunError::MpoError(msg)) => {
eprintln!("{msg}");
ExitCode::FAILURE
}
Err(RunError::PortraitError(msg)) => {
eprintln!("{msg}");
ExitCode::FAILURE
}
Err(err @ RunError::MpoRead { .. }) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Err(err @ RunError::PortraitRead { .. }) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Err(RunError::InputRequired(msg)) => {
eprintln!("{msg}");
ExitCode::FAILURE
}
Err(RunError::AudioError(msg)) => {
eprintln!("{msg}");
ExitCode::FAILURE
}
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
}
}
fn warn_unused_flags(cli: &Cli, core_filter: CoreFilter) {
if !matches!(core_filter, CoreFilter::Astigmatism { .. }) && cli.axis != 90.0 {
eprintln!(
"sensus: warning: --axis is only used with --filter astigmatism (ignored for {core_filter:?})"
);
}
let uses_seed = matches!(
core_filter,
CoreFilter::Cataract { .. }
| CoreFilter::Floaters { .. }
| CoreFilter::FlickeringStars { .. }
);
if !uses_seed && cli.seed != 0 {
eprintln!(
"sensus: warning: --seed is only used with --filter cataract / floaters / flickering-stars (ignored for {core_filter:?})"
);
}
let uses_floater_params = matches!(core_filter, CoreFilter::Floaters { .. });
if !uses_floater_params
&& (cli.density != 0.5 || cli.gaze_x != 0.5 || cli.gaze_y != 0.5 || cli.size != 1.0)
{
eprintln!(
"sensus: warning: --density/--gaze-x/--gaze-y/--size are only used with --filter floaters (ignored for {core_filter:?})"
);
}
if !matches!(core_filter, CoreFilter::Hemianopia { .. }) && cli.side != 0.0 {
eprintln!(
"sensus: warning: --side is only used with --filter hemianopia (ignored for {core_filter:?})"
);
}
}
fn run(cli: Cli) -> Result<(), RunError> {
if let Some(audio_path) = cli.audio.clone() {
return run_audio(&cli, &audio_path);
}
if cli.pipe {
return run_pipe(&cli);
}
if let Some(mpo_path) = cli.mpo.clone() {
let kinds = depth_kinds(&cli);
if kinds.len() != 1 {
return Err(RunError::MpoError(if kinds.is_empty() {
"sensus: --mpo requires a depth blur filter (myopia-depth, hyperopia-depth, depth-of-field)".to_string()
} else {
"sensus: --mpo accepts at most one depth blur filter".to_string()
}));
}
let kind = kinds[0];
if cli.depth.is_some() {
return Err(RunError::MpoError(
"sensus: --mpo and --depth cannot be used together".to_string(),
));
}
let bytes = std::fs::read(&mpo_path).map_err(|source| RunError::MpoRead {
path: mpo_path.clone(),
source,
})?;
let (left, right) =
split_mpo(&bytes).map_err(|e| RunError::MpoError(format!("sensus: {e}")))?;
let depth_img = stereo_to_depth(&left, &right)
.map_err(|e| RunError::MpoError(format!("sensus: {e}")))?;
let base = apply_non_depth_filters(left, &cli)?;
let out = depth_aware_blur(
base,
&depth_img,
cli.focus,
cli.strength * DEPTH_BLUR_MAX_RADIUS_RATIO,
kind,
)
.map_err(|e| RunError::Pipeline(format!("sensus: {e}")))?;
let out_path = cli.output.as_ref().unwrap();
return out.save(out_path).map_err(|source| RunError::OutputSave {
path: out_path.clone(),
source,
});
}
if let Some(portrait_path) = cli.portrait.clone() {
let kinds = depth_kinds(&cli);
if kinds.len() != 1 {
return Err(RunError::PortraitError(if kinds.is_empty() {
"sensus: --portrait requires a depth blur filter (myopia-depth, hyperopia-depth, depth-of-field)".to_string()
} else {
"sensus: --portrait accepts at most one depth blur filter".to_string()
}));
}
let kind = kinds[0];
if cli.depth.is_some() {
return Err(RunError::PortraitError(
"sensus: --portrait and --depth cannot be used together".to_string(),
));
}
let portrait_bytes =
std::fs::read(&portrait_path).map_err(|source| RunError::PortraitRead {
path: portrait_path.clone(),
source,
})?;
let depth_map = read_xmp_depth(&portrait_bytes)
.map_err(|e| RunError::PortraitError(format!("sensus: {e}")))?;
let source_img = if let Some(ref inp) = cli.input {
image::open(inp).map_err(|source| RunError::InputOpen {
path: inp.clone(),
source,
})?
} else {
image::load_from_memory(&portrait_bytes).map_err(|source| RunError::InputOpen {
path: portrait_path.clone(),
source,
})?
};
let base = apply_non_depth_filters(source_img, &cli)?;
let out = depth_aware_blur(
base,
&depth_map,
cli.focus,
cli.strength * DEPTH_BLUR_MAX_RADIUS_RATIO,
kind,
)
.map_err(|e| RunError::PortraitError(format!("sensus: {e}")))?;
let out_path = cli.output.as_ref().unwrap();
return out.save(out_path).map_err(|source| RunError::OutputSave {
path: out_path.clone(),
source,
});
}
let input_path = cli.input.clone().ok_or_else(|| {
RunError::InputRequired(
"sensus: --input is required when --mpo and --portrait are not specified".to_string(),
)
})?;
let img = image::open(&input_path).map_err(|source| RunError::InputOpen {
path: input_path.clone(),
source,
})?;
if cli.filter.iter().any(|f| f.is_depth_filter()) {
let kinds = depth_kinds(&cli);
if kinds.len() != 1 {
return Err(RunError::Pipeline(
"sensus: exactly one depth blur filter is allowed (myopia-depth / hyperopia-depth / depth-of-field)".to_string(),
));
}
let kind = kinds[0];
let depth_path = cli.depth.as_ref().ok_or_else(|| {
RunError::Pipeline(
"sensus: --depth <PATH> is required for depth blur filters".to_string(),
)
})?;
let depth_img = image::open(depth_path).map_err(|source| RunError::InputOpen {
path: depth_path.clone(),
source,
})?;
let base = apply_non_depth_filters(img, &cli)?;
let out = depth_aware_blur(
base,
&depth_img,
cli.focus,
cli.strength * DEPTH_BLUR_MAX_RADIUS_RATIO,
kind,
)
.map_err(|e| RunError::Pipeline(format!("sensus: {e}")))?;
let out_path = cli.output.as_ref().unwrap();
return out.save(out_path).map_err(|source| RunError::OutputSave {
path: out_path.clone(),
source,
});
}
let mut pipeline = Pipeline::new();
for f in &cli.filter {
let core_filter = f.to_core(&cli);
pipeline = pipeline.push(FilterStep::new(core_filter, cli.strength));
}
if cli.filter.len() == 1 {
warn_unused_flags(&cli, cli.filter[0].to_core(&cli));
}
let result = pipeline.apply(img);
match result {
Ok(out) => {
let out_path = cli.output.as_ref().unwrap();
out.save(out_path).map_err(|source| RunError::OutputSave {
path: out_path.clone(),
source,
})?;
Ok(())
}
Err(CoreError::Image(err)) => Err(RunError::InputOpen {
path: input_path.clone(),
source: err,
}),
Err(e) => Err(RunError::Pipeline(format!("sensus: {e}"))),
}
}
fn run_audio(cli: &Cli, audio_path: &Path) -> Result<(), RunError> {
if cli.hearing.is_empty() {
return Err(RunError::AudioError(
"sensus: --audio requires at least one --hearing filter".to_string(),
));
}
if !cli.filter.is_empty() {
return Err(RunError::AudioError(
"sensus: --audio (hearing) cannot be combined with --filter (image filters)"
.to_string(),
));
}
let out_path = cli.output.as_ref().ok_or_else(|| {
RunError::AudioError("sensus: --output <PATH> is required with --audio".to_string())
})?;
let (buf, spec) = audio::read_wav(audio_path).map_err(RunError::AudioError)?;
let mut pipeline = AudioPipeline::new();
for h in &cli.hearing {
pipeline = pipeline.push(h.to_core(cli), cli.strength);
}
let out = pipeline
.apply(&buf)
.map_err(|e| RunError::AudioError(format!("sensus: {e}")))?;
audio::write_wav(out_path, &out, spec).map_err(RunError::AudioError)
}
fn apply_filters_to_image(
img: image::DynamicImage,
cli: &Cli,
) -> Result<image::DynamicImage, RunError> {
let mut pipeline = Pipeline::new();
for f in &cli.filter {
let core_filter = f.to_core(cli);
pipeline = pipeline.push(FilterStep::new(core_filter, cli.strength));
}
pipeline
.apply(img)
.map_err(|e| RunError::Pipeline(format!("sensus: {e}")))
}
fn depth_kinds(cli: &Cli) -> Vec<DepthBlurKind> {
cli.filter.iter().filter_map(|f| f.depth_kind()).collect()
}
fn apply_non_depth_filters(
img: image::DynamicImage,
cli: &Cli,
) -> Result<image::DynamicImage, RunError> {
let mut pipeline = Pipeline::new();
for f in cli.filter.iter().filter(|f| !f.is_depth_filter()) {
let core_filter = f.to_core(cli);
pipeline = pipeline.push(FilterStep::new(core_filter, cli.strength));
}
pipeline
.apply(img)
.map_err(|e| RunError::Pipeline(format!("sensus: {e}")))
}
fn run_pipe(args: &Cli) -> Result<(), RunError> {
use std::io::{Read, Write};
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut reader = stdin.lock();
let mut writer = stdout.lock();
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
let frames = split_jpeg_frames(&buf);
for frame_data in frames {
let img = image::load_from_memory(frame_data).map_err(|source| RunError::InputOpen {
path: std::path::PathBuf::from("<stdin>"),
source,
})?;
let out = apply_filters_to_image(img, args)?;
let mut jpeg_buf = Vec::new();
out.write_to(
&mut std::io::Cursor::new(&mut jpeg_buf),
image::ImageFormat::Jpeg,
)
.map_err(|source| RunError::OutputSave {
path: std::path::PathBuf::from("<stdout>"),
source,
})?;
writer.write_all(&jpeg_buf)?;
}
Ok(())
}
fn split_jpeg_frames(data: &[u8]) -> Vec<&[u8]> {
let mut frames = Vec::new();
let mut i = 0;
while i + 1 < data.len() {
if data[i] != 0xFF || data[i + 1] != 0xD8 {
i += 1;
continue;
}
let frame_start = i;
i += 2; 'frame: loop {
while i < data.len() && data[i] != 0xFF {
i += 1;
}
if i + 1 >= data.len() {
break 'frame;
}
let marker = data[i + 1];
match marker {
0xD9 => {
frames.push(&data[frame_start..=i + 1]);
i += 2;
break 'frame;
}
0xD8 => {
i += 2;
}
0x00 | 0xFF => {
i += 1;
}
0xD0..=0xD7 | 0x01 => {
i += 2;
}
_ => {
if i + 3 >= data.len() {
break 'frame;
}
let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
i += 2 + len; }
}
}
}
frames
}