1use crate::midi::Assembler;
5use crate::state::Origin;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Entry {
9 Mark { ms: u64, text: String },
10 Message { ms: u64, origin: Origin, bytes: Vec<u8> },
11}
12
13pub fn parse(text: &str) -> Vec<Entry> {
15 let (mut tx, mut rx) = (Assembler::default(), Assembler::default());
16 let mut out = vec![];
17 for line in text.lines() {
18 if let Some(rest) = line.strip_prefix("# MARK ") {
19 let (ms, text) = rest.split_once(' ').unwrap_or((rest, ""));
20 out.push(Entry::Mark { ms: ms.parse().unwrap_or(0), text: text.to_owned() });
21 continue;
22 }
23 if line.starts_with('#') {
24 continue;
25 }
26 let mut parts = line.splitn(3, ' ');
27 let (Some(ms), Some(dir), Some(rest)) = (parts.next(), parts.next(), parts.next()) else {
28 continue;
29 };
30 let (origin, asm) = match dir {
31 "TX" => (Origin::Host, &mut tx),
32 "RX" | "MON" => (Origin::Pedal, &mut rx),
33 _ => continue,
34 };
35 let Some(hex) = rest.split_once(']').map(|(_, h)| h) else { continue };
36 let data: Vec<u8> = hex.split_whitespace().filter_map(|h| u8::from_str_radix(h, 16).ok()).collect();
37 let ms = ms.parse().unwrap_or(0);
38 for bytes in asm.push(&data) {
39 out.push(Entry::Message { ms, origin, bytes });
40 }
41 }
42 out
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::catalog::Module;
49 use crate::state::{Category, DeviceState};
50
51 fn main_session() -> Vec<Entry> {
52 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/captures/logs/01-main-session.log");
53 parse(&std::fs::read_to_string(path).expect("capture log"))
54 }
55
56 #[test]
57 fn replay_main_session() {
58 let mut st = DeviceState::default();
59 let mut unknown = 0;
60 let mut handshake_done = false;
61 for e in main_session() {
62 let Entry::Message { origin, bytes, .. } = e else { continue };
63 let ev = st.apply(&bytes, origin);
64 if ev.category == Category::Unknown {
65 unknown += 1;
66 }
67 if !handshake_done && st.presets.iter().all(|p| p.is_some()) {
68 handshake_done = true;
69 assert_eq!(st.firmware.as_deref(), Some("V1.1.1"));
70 assert_eq!(st.model_id.as_deref(), Some("JME-01"));
71 assert_eq!(st.product.as_deref(), Some("JME STOMPBOX"));
72 for (i, p) in st.presets.iter().enumerate() {
73 let p = p.as_ref().unwrap();
74 assert_eq!(p.index as usize, i);
75 assert!(!p.name.is_empty(), "preset {i} has a name");
76 assert!(p.name.chars().all(|c| c.is_ascii_graphic() || c == ' '), "{:?}", p.name);
77 assert!(p.modules[Module::Amp.index()].model < 40);
78 }
79 assert!(st.live_known);
81 }
82 }
83 assert!(handshake_done);
84 assert!(unknown < 10, "{unknown} unknown messages");
85 let s = st.settings.expect("settings");
86 assert!(s.input_trim <= 20);
87 }
88
89 #[test]
92 fn records_round_trip() {
93 use crate::midi::{Msg, host};
94 use crate::records::{Preset, Settings, param_count};
95 let (mut presets, mut settings) = (0, 0);
96 for e in main_session() {
97 let Entry::Message { bytes, .. } = e else { continue };
98 let Msg::Nep { cmd, dir, payload } = Msg::parse(&bytes) else { continue };
99 if dir != 1 && dir != 2 {
100 continue;
101 }
102 match cmd {
103 0x0B | 0x0C => {
104 let p = Preset::from_packed(&payload).unwrap();
105 assert_eq!(p.to_packed(), payload, "preset {}", p.name);
106 for m in Module::EFFECTS {
107 let count = p.raw[[25, 32, 41, 61, 66, 73, 20, 48][m.index()]];
108 let model = p.modules[m.index()].model;
109 assert_eq!(count, param_count(m, model), "{} {} model {model}", p.name, m.name());
110 }
111 if dir == 1 {
112 assert_eq!(host::preset_write(&p), bytes);
113 }
114 presets += 1;
115 }
116 0x14 => {
117 let s = Settings::from_packed(&payload).unwrap();
118 assert_eq!(s.to_packed(), payload);
119 if dir == 1 {
120 assert_eq!(host::settings_write(&s), bytes);
121 }
122 settings += 1;
123 }
124 _ => {}
125 }
126 }
127 assert!(presets >= 100 && settings > 100, "{presets} presets, {settings} settings");
128 }
129
130 #[test]
132 fn save_as_matches_capture() {
133 use crate::midi::host;
134 let entries = main_session();
135 let save_at = entries
136 .iter()
137 .position(|e| matches!(e, Entry::Message { origin: Origin::Host, bytes, .. } if bytes.get(5) == Some(&0x12)))
138 .unwrap();
139 let mut st = DeviceState::default();
140 for e in &entries[..save_at] {
141 if let Entry::Message { origin, bytes, .. } = e {
142 st.apply(bytes, *origin);
143 }
144 }
145 assert_eq!(st.current, Some(0x44));
146 let mut p = st.live.clone();
147 p.index = 0x15;
148 p.name = "4Ooz!".into();
149 let ours = host::save_preset(st.current.unwrap(), &p);
150 let theirs: Vec<Vec<u8>> = entries[save_at..]
151 .iter()
152 .filter_map(|e| match e {
153 Entry::Message { origin: Origin::Host, bytes, .. } => Some(bytes.clone()),
154 _ => None,
155 })
156 .take(3)
157 .collect();
158 assert_eq!(ours, theirs);
159 }
160
161 #[test]
164 fn uploads_match_capture() {
165 use crate::midi::{Cmd, Msg};
166 use crate::upload::{self, Chunk, Reassembler, UserAmpInfo, amp_sub};
167
168 let host: Vec<Vec<u8>> = main_session()
169 .into_iter()
170 .filter_map(|e| match e {
171 Entry::Message { origin: Origin::Host, bytes, .. } => Some(bytes),
172 _ => None,
173 })
174 .collect();
175 let nep = |b: &[u8]| match Msg::parse(b) {
176 Msg::Nep { cmd, dir: 1, payload } => Some((cmd, payload)),
177 _ => None,
178 };
179 let collect = |from: usize| -> (usize, u8, Vec<f32>) {
181 let mut r = Reassembler::default();
182 for (k, b) in host.iter().enumerate().skip(from) {
183 let (_, p) = nep(b).expect("data chunk");
184 if let Some((slot, floats)) = r.push(&Chunk::parse(&p).unwrap()) {
185 return (k, slot, floats);
186 }
187 }
188 panic!("incomplete upload");
189 };
190 let (mut amps, mut irs) = (0, 0);
191 let mut i = 0;
192 while i < host.len() {
193 let Some((cmd, payload)) = nep(&host[i]) else {
194 i += 1;
195 continue;
196 };
197 let chunk = Chunk::parse(&payload);
198 if cmd == Cmd::UserAmp as u8 && let Some(info) = UserAmpInfo::parse(&payload) {
199 let (end, slot, floats) = collect(i + 1);
201 assert_eq!((slot, floats.len(), info.factor), (info.slot, upload::UPLOAD_FLOATS, 1.0));
202 let ours = upload::user_amp_upload(&UserAmpInfo::new(info.slot, &info.name, info.factor), &floats);
203 assert_eq!(ours, host[i..=end], "NAM import {:?}", info.name);
204 amps += 1;
205 i = end;
206 } else if cmd == Cmd::IrData as u8 && chunk.as_ref().is_some_and(|c| c.offset == 0) {
207 let (end, slot, floats) = collect(i);
209 let name = crate::records::parse_ir_slot(&nep(&host[end + 1]).unwrap().1).unwrap().1;
210 assert_eq!(upload::user_ir_upload(slot, &name, &floats), host[i..=end + 1], "IR import {name:?}");
211 irs += 1;
212 i = end + 1;
213 } else if cmd == Cmd::UserAmp as u8 && let Some(c) = chunk.filter(|c| c.sub == amp_sub::DELETE) {
214 assert_eq!(upload::user_amp_delete(c.slot), host[i]);
215 }
216 i += 1;
217 }
218 assert_eq!((amps, irs), (3, 1), "FNDR twice + Jcm800, one IR");
219
220 let mut st = DeviceState::default();
221 for e in main_session() {
222 if let Entry::Message { origin, bytes, .. } = e {
223 st.apply(&bytes, origin);
224 }
225 }
226 assert_eq!(st.user_amps[0].as_deref(), Some("FNDR BFSR VB Edge WRM2 CAB FREE"));
227 assert_eq!(st.user_amps[2].as_deref(), Some("Jcm800"));
228 assert_eq!(st.user_irs[0].as_deref(), Some("Delay And Reverb"));
229 }
230
231 #[test]
232 fn preset_switch_applies_cached_preset() {
233 let mut st = DeviceState::default();
234 for e in main_session() {
235 if let Entry::Message { origin, bytes, .. } = e {
236 st.apply(&bytes, origin);
237 if bytes[0] & 0xF0 == 0xC0 && st.presets.iter().all(|p| p.is_some()) {
238 let i = bytes[1];
239 assert_eq!(st.current, Some(i));
240 assert_eq!(st.live.name, st.presets[i as usize].as_ref().unwrap().name);
241 }
242 }
243 }
244 }
245}