1use super::{command, frame_with, indexed, BROADCAST};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct Record {
12 pub addr: u16,
13 pub len: u16,
14 pub name: &'static str,
15}
16
17pub const RECORDS: &[Record] = &[
20 Record { addr: 0x000, len: 2, name: "debug bytes" },
21 Record { addr: 0x002, len: 42, name: "control area" },
22 Record { addr: 0x02c, len: 18, name: "colour-gamut coefficients" },
23 Record { addr: 0x03e, len: 1, name: "gamut-adjust enable" },
24 Record { addr: 0x040, len: 1, name: "calibration status" },
25 Record { addr: 0x041, len: 1, name: "no-input show info" },
26 Record { addr: 0x042, len: 1, name: "turn-on screen show" },
27 Record { addr: 0x043, len: 3, name: "white-balance adjust" },
28 Record { addr: 0x04b, len: 1, name: "calibration-coefficient source" },
29 Record { addr: 0x04c, len: 1, name: "seam enable" },
30 Record { addr: 0x04d, len: 9, name: "unresolved (wall dims in the corpus)" },
31 Record { addr: 0x056, len: 3, name: "void-line info" },
32 Record { addr: 0x059, len: 1, name: "receiver-card light" },
33 Record { addr: 0x05a, len: 20, name: "receiver card name" },
34 Record { addr: 0x06e, len: 1, name: "14-way open flag" },
35 Record { addr: 0x06f, len: 1, name: "gamma-calibration status" },
36 Record { addr: 0x070, len: 1, name: "ROE current/bright flag" },
37 Record { addr: 0x072, len: 1, name: "virtual-pixel param" },
38 Record { addr: 0x076, len: 1, name: "full-screen seam-factor enable" },
39 Record { addr: 0x077, len: 1, name: "four-deseam" },
40 Record { addr: 0x07b, len: 1, name: "plus-module 7-way adjust enable" },
41 Record { addr: 0x07c, len: 1, name: "double-cali chroma enable" },
42 Record { addr: 0x07d, len: 1, name: "plus low-bright cali enable" },
43 Record { addr: 0x07e, len: 1, name: "double-cali enable" },
44 Record { addr: 0x07f, len: 2, name: "double-cali threshold" },
45 Record { addr: 0x092, len: 32, name: "control-area blob, high half" },
46 Record { addr: 0x0b2, len: 1, name: "parameter switch" },
47 Record { addr: 0x0b3, len: 1, name: "plus-chip low-bright cali enable" },
48 Record { addr: 0x0b4, len: 3, name: "plus-chip low-bright uniformity" },
49 Record { addr: 0x0c1, len: 12, name: "GX custom FCCL" },
50 Record { addr: 0x0c8, len: 1, name: "plus temperature-control enable" },
51 Record { addr: 0x0ce, len: 16, name: "double-cali threshold (long form)" },
52 Record { addr: 0x0e1, len: 1, name: "plus-module current-adjust enable" },
53 Record { addr: 0x0f4, len: 2, name: "preset temperature / ROE fan" },
54 Record { addr: 0x0f6, len: 1, name: "power-off bright coefficient" },
55 Record { addr: 0x0f7, len: 2, name: "EMC info" },
56 Record { addr: 0x0f9, len: 1, name: "module power switch" },
57 Record { addr: 0x0fa, len: 1, name: "current/bright flag" },
58 Record { addr: 0x0fd, len: 1, name: "screen-shake param" },
59];
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct ControlArea {
65 pub start_x: u16,
66 pub start_y: u16,
67 pub end_x: u16,
68 pub end_y: u16,
69}
70
71impl ControlArea {
72 #[must_use]
74 pub const fn for_cabinet(x: u16, y: u16, w: u16, h: u16) -> Self {
75 Self { start_x: x, start_y: y, end_x: x + w, end_y: y + h }
76 }
77
78 #[must_use]
80 pub fn to_record(self) -> [u8; 42] {
81 let mut r = [0u8; 42];
82 r[0..2].copy_from_slice(&self.start_x.to_be_bytes());
83 r[2..4].copy_from_slice(&self.start_y.to_be_bytes());
84 r[4..6].copy_from_slice(&self.end_x.to_be_bytes());
85 r[6..8].copy_from_slice(&self.end_y.to_be_bytes());
86 r
87 }
88
89 #[must_use]
90 pub fn parse(r: &[u8]) -> Option<Self> {
91 if r.len() < 8 {
92 return None;
93 }
94 let u = |i: usize| u16::from_be_bytes([r[i], r[i + 1]]);
95 Some(Self { start_x: u(0), start_y: u(2), end_x: u(4), end_y: u(6) })
96 }
97}
98
99#[must_use]
101pub fn control_area(x: u16, y: u16, w: u16, h: u16) -> [u8; 42] {
102 ControlArea::for_cabinet(x, y, w, h).to_record()
103}
104
105#[must_use]
107pub fn parse_control_area(r: &[u8]) -> Option<(u16, u16, u16, u16)> {
108 ControlArea::parse(r).map(|a| (a.start_x, a.start_y, a.end_x, a.end_y))
109}
110
111fn record_frame(index: u16, opcode: u8, addr: u32, data: &[u8]) -> Vec<u8> {
114 let n = (data.len() + 0x12).max(0x80);
117 frame_with([0x19, 0x00], n, |p| {
118 indexed(p, index, opcode);
119 p[4..8].copy_from_slice(&addr.to_be_bytes());
120 p[8..12].copy_from_slice(&(data.len() as u32).to_be_bytes());
121 p[12..12 + data.len()].copy_from_slice(data);
122 })
123}
124
125#[must_use]
128pub fn write_to(index: u16, addr: u16, data: &[u8]) -> Vec<u8> {
129 record_frame(index, 0x85, u32::from(addr), data)
130}
131
132#[must_use]
134pub fn write(addr: u16, data: &[u8]) -> Vec<u8> {
135 write_to(BROADCAST, addr, data)
136}
137
138#[must_use]
140pub fn save_to(index: u16) -> Vec<u8> {
141 record_frame(index, 0x87, 0, &[])
142}
143
144#[must_use]
146pub fn save() -> Vec<u8> {
147 save_to(BROADCAST)
148}
149
150#[must_use]
154pub fn reload_to(index: u16) -> Vec<u8> {
155 command(index, 0x77, &[0x01, 0x01, 0x00])
156}
157
158#[must_use]
160pub fn reload() -> Vec<u8> {
161 reload_to(BROADCAST)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn control_area_is_big_endian_corners() {
170 let r = control_area(0, 0, 128, 64);
171 assert_eq!(&r[..8], &[0, 0, 0, 0, 0, 128, 0, 64]);
172 assert_eq!(parse_control_area(&r), Some((0, 0, 128, 64)));
173 let r = control_area(128, 64, 128, 64);
174 assert_eq!(parse_control_area(&r), Some((128, 64, 256, 128)));
175 }
176
177 #[test]
178 fn control_area_round_trips_through_its_corners() {
179 let area = ControlArea::for_cabinet(128, 64, 128, 64);
180 assert_eq!(area, ControlArea { start_x: 128, start_y: 64, end_x: 256, end_y: 128 });
181 assert_eq!(ControlArea::parse(&area.to_record()), Some(area));
182 assert_eq!(ControlArea::parse(&[0; 7]), None);
183 }
184
185 #[test]
186 fn write_frame_matches_the_vendor_layout() {
187 let f = write(0x02, &[0u8; 42]);
188 assert_eq!(&f[12..14], &[0x19, 0x00]);
189 assert_eq!(&f[15..17], &[0xff, 0xff], "broadcast index");
190 assert_eq!(f[17], 0x85);
191 assert_eq!(&f[18..22], &[0, 0, 0, 2]);
192 assert_eq!(&f[22..26], &[0, 0, 0, 42]);
193 assert_eq!(f.len(), 14 + 0x80, "payload padded to 0x80");
194 }
195
196 #[test]
197 fn indexed_frames_carry_the_chain_position_big_endian() {
198 let f = write_to(0x0102, 0x02, &[0u8; 42]);
200 assert_eq!(&f[15..17], &[0x01, 0x02]);
201 assert_eq!(f[17], 0x85);
202 let f = save_to(3);
203 assert_eq!(&f[15..18], &[0x00, 0x03, 0x87]);
204 assert_eq!(&f[18..26], &[0; 8], "save carries no address or length");
205 let f = reload_to(3);
206 assert_eq!(&f[12..14], &[0x06, 0x00]);
207 assert_eq!(&f[15..18], &[0x00, 0x03, 0x77]);
208 assert_eq!(&f[22..25], &[0x01, 0x01, 0x00]);
209 }
210
211 #[test]
212 fn broadcast_forms_are_the_indexed_forms_at_0xffff() {
213 assert_eq!(write(0x02, &[7u8; 42]), write_to(BROADCAST, 0x02, &[7u8; 42]));
214 assert_eq!(save(), save_to(BROADCAST));
215 assert_eq!(reload(), reload_to(BROADCAST));
216 assert_ne!(write(0x02, &[0u8; 42]), write_to(0, 0x02, &[0u8; 42]));
217 }
218
219 #[test]
220 fn reload_matches_the_vendor_bytes() {
221 let f = reload();
222 assert_eq!(f.len(), 140);
223 assert_eq!(&f[12..14], &[0x06, 0x00]);
224 assert_eq!(&f[15..18], &[0xff, 0xff, 0x77]);
225 assert_eq!(&f[22..25], &[0x01, 0x01, 0x00]);
226 assert!(f[25..].iter().all(|&b| b == 0));
227 }
228
229 #[test]
230 fn records_are_in_address_order() {
231 for w in RECORDS.windows(2) {
233 assert!(w[1].addr > w[0].addr, "{} out of order", w[1].name);
234 }
235 }
236}