1pub const HISTORY_DAYS: u32 = 30;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Kind {
27 Double,
28 Int,
29 Enum,
30 Text,
31 Waveform(u32),
32}
33
34impl Kind {
35 pub fn dbr_type(self) -> &'static str {
37 match self {
38 Kind::Double => "DBR_SCALAR_DOUBLE",
39 Kind::Int => "DBR_SCALAR_INT",
40 Kind::Enum => "DBR_SCALAR_ENUM",
41 Kind::Text => "DBR_SCALAR_STRING",
42 Kind::Waveform(_) => "DBR_WAVEFORM_DOUBLE",
43 }
44 }
45
46 pub fn element_count(self) -> u32 {
48 match self {
49 Kind::Waveform(n) => n,
50 _ => 1,
51 }
52 }
53
54 pub fn is_scalar_numeric(self) -> bool {
56 matches!(self, Kind::Double | Kind::Int | Kind::Enum)
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Shape {
63 Sine,
64 Ramp,
65 Noise,
66 Slow,
67 Step,
68 Enum,
69 Text,
70 Wave,
71 Nan,
72 Big,
73 BeamA,
74 BeamB,
75 BeamSum,
76 Motor1,
77 Motor2,
78}
79
80#[derive(Clone, Copy, Debug)]
82pub struct Signal {
83 pub name: &'static str,
84 pub kind: Kind,
85 pub period: f64,
87 pub egu: &'static str,
88 pub prec: u32,
89 pub desc: &'static str,
91 pub lopr: f64,
93 pub hopr: f64,
94 pub shape: Shape,
95}
96
97#[allow(clippy::too_many_arguments)]
98const fn sig(
99 name: &'static str,
100 kind: Kind,
101 period: f64,
102 egu: &'static str,
103 prec: u32,
104 desc: &'static str,
105 range: (f64, f64),
106 shape: Shape,
107) -> Signal {
108 Signal {
109 name,
110 kind,
111 period,
112 egu,
113 prec,
114 desc,
115 lopr: range.0,
116 hopr: range.1,
117 shape,
118 }
119}
120
121#[rustfmt::skip]
122static ALL: [Signal; 15] = [
123 sig("MOCK:SINE", Kind::Double, 1.0, "mA", 3, "Sine, 10 minute period", (200.0, 300.0), Shape::Sine),
124 sig("MOCK:RAMP", Kind::Double, 10.0, "%", 1, "Sawtooth over an hour", (0.0, 100.0), Shape::Ramp),
125 sig("MOCK:NOISE", Kind::Double, 0.1, "V", 4, "Noise on a slow drift", (-10.0, 10.0), Shape::Noise),
126 sig("MOCK:SLOW", Kind::Double, 300.0, "degC", 2, "Every five minutes", (15.0, 25.0), Shape::Slow),
127 sig("MOCK:STEP", Kind::Double, 1.0, "mm", 2, "Holds, then jumps", (0.0, 10.0), Shape::Step),
128 sig("MOCK:ENUM", Kind::Enum, 60.0, "", 0, "Mode 0..3", (0.0, 3.0), Shape::Enum),
129 sig("MOCK:STRING", Kind::Text, 600.0, "", 0, "Status text", (0.0, 0.0), Shape::Text),
130 sig("MOCK:WAVE", Kind::Waveform(8), 10.0, "counts", 1, "8-element waveform", (-1.0, 1.0), Shape::Wave),
131 sig("MOCK:NAN", Kind::Double, 1.0, "mA", 3, "Sine with NaNs and disconnects", (-1.0, 1.0), Shape::Nan),
132 sig("MOCK:BIG", Kind::Double, 1.0, "Pa", 3, "Large values", (-3e10, 3e10), Shape::Big),
133 sig("bl01t:A", Kind::Double, 10.0, "", 3, "Input A", (0.0, 10.0), Shape::BeamA),
134 sig("bl01t:B", Kind::Double, 10.0, "", 3, "Input B", (0.0, 10.0), Shape::BeamB),
135 sig("bl01t:SUM", Kind::Double, 10.0, "", 3, "Sum A and B", (0.0, 20.0), Shape::BeamSum),
136 sig("BL01T-MO-SIMC-01:M1.RBV", Kind::Double, 1.0, "mm", 3, "Motor 1 readback", (-10.0, 10.0), Shape::Motor1),
137 sig("BL01T-MO-SIMC-01:M2.RBV", Kind::Double, 1.0, "mm", 3, "Motor 2 readback", (-10.0, 10.0), Shape::Motor2),
138];
139
140pub fn all() -> &'static [Signal] {
142 &ALL
143}
144
145pub fn find(name: &str) -> Option<&'static Signal> {
147 all().iter().find(|s| s.name == name)
148}
149
150#[derive(Clone, Debug, PartialEq)]
152pub enum Value {
153 Double(f64),
154 Int(i64),
155 Text(String),
156 Array(Vec<f64>),
157}
158
159impl Value {
160 pub fn as_f64(&self) -> Option<f64> {
162 match self {
163 Value::Double(v) => Some(*v),
164 Value::Int(i) => Some(*i as f64),
165 Value::Text(_) => None,
166 Value::Array(a) => Some(a.first().copied().unwrap_or(f64::NAN)),
167 }
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct Event {
174 pub t_ns: i64,
176 pub value: Value,
177 pub severity: u8,
178 pub status: u16,
179 pub fields: Vec<(&'static str, String)>,
181}
182
183pub const NAN_CYCLE: i64 = 2000;
187pub const NAN_LOST: i64 = 1000;
188pub const NAN_REGAINED: i64 = 1060;
189
190fn hash(mut x: u64) -> u64 {
192 x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
193 x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
194 x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
195 x ^ (x >> 31)
196}
197
198fn unit(k: i64, salt: u64) -> f64 {
200 (hash(k as u64 ^ salt.rotate_left(17)) >> 11) as f64 / (1u64 << 53) as f64
201}
202
203fn normal(k: i64, salt: u64) -> f64 {
205 let s: f64 = (0..4)
206 .map(|i| unit(k, salt.wrapping_add(i).wrapping_mul(0x1000_0001)))
207 .sum();
208 (s - 2.0) * 3.0f64.sqrt()
209}
210
211fn motor(t: f64, salt: u64) -> f64 {
215 const MOVE: f64 = 900.0;
216 const TRAVEL: f64 = 20.0;
217 const AWAY: f64 = 120.0;
218 let n = (t / MOVE).floor() as i64;
219 let pos = ((unit(n, salt) * 16.0 - 8.0) * 100.0).round() / 100.0;
220 let s = t - n as f64 * MOVE;
221 let out = (s / TRAVEL).clamp(0.0, 1.0);
222 let back = ((s - TRAVEL - AWAY) / TRAVEL).clamp(0.0, 1.0);
223 pos * (out - back)
224}
225
226const TAU: f64 = std::f64::consts::TAU;
227
228impl Signal {
229 pub fn period_ns(&self) -> i64 {
231 (self.period * 1e9).round() as i64
232 }
233
234 pub fn event(&self, k: i64) -> Option<Event> {
237 let t_ns = k * self.period_ns();
238 let t = t_ns as f64 * 1e-9;
239 let mut severity = 0;
240 let mut status = 0;
241 let mut fields = Vec::new();
242 let salt = self
243 .name
244 .bytes()
245 .fold(0u64, |h, b| h.wrapping_mul(31) ^ b as u64);
246 let value = match self.shape {
247 Shape::Sine => {
248 let v = 250.0 + 40.0 * (TAU * t / 600.0).sin() + 0.2 * normal(k, salt);
249 if v > 285.0 {
250 severity = 1;
251 status = 4; }
253 Value::Double(v)
254 }
255 Shape::Ramp => Value::Double((t % 3600.0) / 36.0),
256 Shape::Noise => Value::Double(5.0 * (TAU * t / 10800.0).sin() + normal(k, salt)),
257 Shape::Slow => {
258 Value::Double(20.0 + 3.0 * (TAU * t / 86400.0).sin() + 0.3 * normal(k, salt))
259 }
260 Shape::Step => {
261 let n = (t / 240.0).floor() as i64;
262 Value::Double((unit(n, salt) * 1000.0).round() / 100.0)
263 }
264 Shape::Enum => Value::Int((hash((t / 1800.0).floor() as u64 ^ salt) % 4) as i64),
265 Shape::Text => {
266 const STATES: [&str; 4] = ["Idle", "Running", "Paused", "Fault: \"door\" open"];
267 Value::Text(STATES[k.rem_euclid(4) as usize].to_string())
268 }
269 Shape::Wave => Value::Array(
270 (0..8)
271 .map(|i| ((TAU * (t / 60.0 + i as f64 / 8.0)).sin() * 10.0).round() / 10.0)
272 .collect(),
273 ),
274 Shape::Nan => {
275 let phase = k.rem_euclid(NAN_CYCLE);
276 if phase > NAN_LOST && phase < NAN_REGAINED {
277 return None;
278 }
279 let lost_secs = (k - phase + NAN_LOST) * self.period_ns() / 1_000_000_000;
280 if phase == NAN_LOST {
281 severity = 3;
282 status = 9; fields.push(("cnxlostepsecs", lost_secs.to_string()));
284 } else if phase == NAN_REGAINED {
285 fields.push(("cnxlostepsecs", lost_secs.to_string()));
286 fields.push(("cnxregainedepsecs", (t_ns / 1_000_000_000).to_string()));
287 }
288 if k.rem_euclid(50) == 49 {
289 Value::Double(f64::NAN)
290 } else {
291 Value::Double((TAU * t / 300.0).sin())
292 }
293 }
294 Shape::Big => Value::Double(3e10 * (TAU * t / 600.0).sin()),
295 Shape::BeamA => Value::Double(beam_a(t)),
296 Shape::BeamB => Value::Double(beam_b(t)),
297 Shape::BeamSum => Value::Double(beam_a(t) + beam_b(t)),
298 Shape::Motor1 => Value::Double(motor(t, salt)),
299 Shape::Motor2 => Value::Double(motor(t + 450.0, salt)),
300 };
301 Some(Event {
302 t_ns,
303 value,
304 severity,
305 status,
306 fields,
307 })
308 }
309}
310
311fn beam_a(t: f64) -> f64 {
314 2.54 + 0.02 * (TAU * t / 7200.0).sin()
315}
316
317fn beam_b(t: f64) -> f64 {
318 2.61 + 0.015 * (TAU * t / 10800.0).cos()
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn names_are_unique_and_findable() {
327 for s in all() {
328 assert_eq!(find(s.name).unwrap().name, s.name);
329 }
330 let mut names: Vec<_> = all().iter().map(|s| s.name).collect();
331 names.sort();
332 names.dedup();
333 assert_eq!(names.len(), all().len());
334 assert!(find("NOPE").is_none());
335 }
336
337 #[test]
338 fn periods_divide_the_history_alignment() {
339 for s in all() {
340 assert_eq!(600_000_000_000 % s.period_ns(), 0, "{}", s.name);
341 }
342 }
343
344 #[test]
345 fn testioc_pvs_rest_at_its_values() {
346 assert_eq!(motor(1_800_000_000.0 + 600.0, 1), 0.0);
348 assert_ne!(motor(1_800_000_000.0 + 60.0, 1), 0.0);
349 let sum = beam_a(1.7e9) + beam_b(1.7e9);
350 assert!((sum - 5.15).abs() < 0.05, "{sum}");
351 }
352
353 #[test]
354 fn values_are_deterministic() {
355 let s = find("MOCK:NOISE").unwrap();
356 assert_eq!(s.event(12345), s.event(12345));
357 assert_ne!(s.event(12345), s.event(12346));
358 }
359
360 #[test]
361 fn nan_pv_has_nans_and_gaps() {
362 let s = find("MOCK:NAN").unwrap();
363 let k0 = 1_700_000_000 / NAN_CYCLE * NAN_CYCLE;
364 let lost = s.event(k0 + NAN_LOST).unwrap();
365 assert_eq!(lost.severity, 3);
366 assert_eq!(lost.fields[0].0, "cnxlostepsecs");
367 assert!(s.event(k0 + NAN_LOST + 1).is_none());
368 assert!(s.event(k0 + NAN_REGAINED - 1).is_none());
369 let back = s.event(k0 + NAN_REGAINED).unwrap();
370 assert_eq!(back.fields.len(), 2);
371 let v = s.event(k0 + 49).unwrap().value.as_f64().unwrap();
372 assert!(v.is_nan());
373 }
374
375 #[test]
376 fn kinds() {
377 assert!(Kind::Enum.is_scalar_numeric());
378 assert!(!Kind::Text.is_scalar_numeric());
379 assert!(!Kind::Waveform(8).is_scalar_numeric());
380 assert_eq!(Kind::Waveform(8).element_count(), 8);
381 let wave = find("MOCK:WAVE").unwrap().event(0).unwrap();
382 assert_eq!(wave.value.as_f64(), Some(0.0));
383 }
384
385 #[test]
386 fn testioc_values_line_up() {
387 let a = find("bl01t:A").unwrap().event(170_000_000).unwrap();
388 let b = find("bl01t:B").unwrap().event(170_000_000).unwrap();
389 let sum = find("bl01t:SUM").unwrap().event(170_000_000).unwrap();
390 let (a, b, sum) = (a.value.as_f64(), b.value.as_f64(), sum.value.as_f64());
391 assert!((a.unwrap() + b.unwrap() - sum.unwrap()).abs() < 1e-12);
392 }
393}