Skip to content

Commit ebaf722

Browse files
authored
Merge pull request #105 from chrivers/chrivers/hue-crate-entertainment-mode
This pull request implements the necessary code in the hue crate, to support decoding and encoding of Hue Entertainment Mode (also known as "Sync mode"). There are two major new areas of support: - Encoding and decoding the DTLS stream going to the bridge (from a sync client) - Encoding and decoding the specialized Zigbee frames going from the bridge, to the lights. Especially the latter part builds on the previous work done by the Bifrost project, to reverse engineer the Hue Entertainment zigbee format. This pull request does not in itself add entertainment mode support to the Bifrost bridge. It only adds required library code needed to implement that support in an upcoming pull request.
2 parents 56b03e0 + 1a4b3fd commit ebaf722

13 files changed

+680
-46
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/hue/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ workspace = true
1818
[dependencies]
1919
bitflags = "2.8.0"
2020
byteorder = "1.5.0"
21+
chrono = { version = "0.4.39", default-features = false }
2122
packed_struct = "0.10.1"
2223
serde = { version = "1.0.217", features = ["derive"] }
2324
thiserror = "2.0.11"
25+
uuid = "1.13.1"
26+
27+
[dev-dependencies]
28+
hex = "0.4.3"

crates/hue/src/colorspace.rs

+166-25
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,121 @@
44
//
55
// Original code by Thomas Lochmatter
66

7+
use std::ops::{Index, IndexMut};
8+
79
use crate::gamma::GammaCorrection;
810

9-
pub struct ColorSpace {
10-
rgb: [f64; 9],
11-
xyz: [f64; 9],
12-
gamma: GammaCorrection,
13-
}
11+
#[derive(Clone, Debug)]
12+
pub struct Matrix3(pub [f64; 3 * 3]);
13+
14+
impl Matrix3 {
15+
#[must_use]
16+
pub const fn identity() -> Self {
17+
Self([
18+
1.0, 0.0, 0.0, //
19+
0.0, 1.0, 0.0, //
20+
0.0, 0.0, 1.0, //
21+
])
22+
}
23+
24+
#[must_use]
25+
pub fn inverted(&self) -> Option<Self> {
26+
let mut current = self.clone();
27+
let mut inverse = Self::identity();
28+
29+
// Gaussian elimination (part 1)
30+
for i in 0..3 {
31+
// Get the diagonal term
32+
let mut d = current[[i, i]];
33+
34+
// If it is 0, there must be at least one row with a non-zero element (otherwise, the matrix is not invertible)
35+
if d == 0.0 {
36+
let mut r = i + 1;
37+
38+
while r < 3 && (current[[r, i]]).abs() < 1e-10 {
39+
r += 1;
40+
}
41+
42+
if r == 3 {
43+
return None;
44+
} // i is the rank
45+
46+
for c in 0..3 {
47+
current[[i, c]] += current[[r, c]];
48+
inverse[[i, c]] += inverse[[r, c]];
49+
}
50+
51+
d = current[[i, i]];
52+
}
53+
54+
// Divide the row by the diagonal term
55+
let inv = 1.0 / d;
56+
for c in 0..3 {
57+
current[[i, c]] *= inv;
58+
inverse[[i, c]] *= inv;
59+
}
60+
61+
// Divide all subsequent rows with a non-zero coefficient, and subtract the row
62+
for r in i + 1..3 {
63+
let p = current.0[r * 3 + i];
64+
if p != 0.0 {
65+
for c in 0..3 {
66+
current[[r, c]] -= current[[i, c]] * p;
67+
inverse[[r, c]] -= inverse[[i, c]] * p;
68+
}
69+
}
70+
}
71+
}
72+
73+
// Gaussian elimination (part 2)
74+
for i in (0..3).rev() {
75+
for r in 0..i {
76+
let d = current[[r, i]];
77+
for c in 0..3 {
78+
current[[r, c]] -= current[[i, c]] * d;
79+
inverse[[r, c]] -= inverse[[i, c]] * d;
80+
}
81+
}
82+
}
83+
84+
Some(inverse)
85+
}
1486

15-
impl ColorSpace {
1687
#[allow(clippy::suboptimal_flops)]
1788
#[must_use]
18-
fn mult(d: [f64; 3], m: &[f64; 9]) -> [f64; 3] {
89+
fn mult(&self, d: [f64; 3]) -> [f64; 3] {
90+
let m = self.0;
1991
let cx = d[0] * m[0] + d[1] * m[1] + d[2] * m[2];
2092
let cy = d[0] * m[3] + d[1] * m[4] + d[2] * m[5];
2193
let cz = d[0] * m[6] + d[1] * m[7] + d[2] * m[8];
2294
[cx, cy, cz]
2395
}
96+
}
97+
98+
impl Index<[usize; 2]> for Matrix3 {
99+
type Output = f64;
100+
101+
fn index(&self, index: [usize; 2]) -> &Self::Output {
102+
&self.0[index[0] * 3 + index[1]]
103+
}
104+
}
24105

106+
impl IndexMut<[usize; 2]> for Matrix3 {
107+
fn index_mut(&mut self, index: [usize; 2]) -> &mut Self::Output {
108+
&mut self.0[index[0] * 3 + index[1]]
109+
}
110+
}
111+
112+
pub struct ColorSpace {
113+
rgb: Matrix3,
114+
xyz: Matrix3,
115+
gamma: GammaCorrection,
116+
}
117+
118+
impl ColorSpace {
25119
#[must_use]
26120
pub fn xyz_to_rgb(&self, x: f64, y: f64, z: f64) -> [f64; 3] {
27-
Self::mult([x, y, z], &self.rgb).map(|q| self.gamma.transform(q))
121+
self.rgb.mult([x, y, z]).map(|q| self.gamma.transform(q))
28122
}
29123

30124
#[allow(non_snake_case)]
@@ -36,7 +130,7 @@ impl ColorSpace {
36130

37131
#[must_use]
38132
pub fn rgb_to_xyz(&self, r: f64, g: f64, b: f64) -> [f64; 3] {
39-
Self::mult([r, g, b].map(|q| self.gamma.inverse(q)), &self.xyz)
133+
self.xyz.mult([r, g, b].map(|q| self.gamma.inverse(q)))
40134
}
41135

42136
#[allow(clippy::many_single_char_names)]
@@ -74,45 +168,92 @@ impl ColorSpace {
74168

75169
/// Wide gamut color space
76170
pub const WIDE: ColorSpace = ColorSpace {
77-
rgb: [
171+
rgb: Matrix3([
78172
1.4625, -0.1845, -0.2734, //
79-
-0.5228, 1.4479, 0.0681, //
173+
-0.5229, 1.4479, 0.0681, //
80174
0.0346, -0.0958, 1.2875, //
81-
],
82-
xyz: [
175+
]),
176+
xyz: Matrix3([
83177
0.7164, 0.1010, 0.1468, //
84178
0.2587, 0.7247, 0.0166, //
85179
0.0000, 0.0512, 0.7740, //
86-
],
180+
]),
87181
gamma: GammaCorrection::NONE,
88182
};
89183

90184
/// sRGB color space
91185
pub const SRGB: ColorSpace = ColorSpace {
92-
rgb: [
93-
3.2405, -1.5371, -0.4985, //
94-
-0.9693, 1.8760, 0.0416, //
95-
0.0556, -0.2040, 1.0572, //
96-
],
97-
xyz: [
186+
rgb: Matrix3([
187+
3.2401, -1.5370, -0.4983, //
188+
-0.9693, 1.8760, 0.0415, //
189+
0.0558, -0.2040, 1.0572, //
190+
]),
191+
xyz: Matrix3([
98192
0.4125, 0.3576, 0.1804, //
99193
0.2127, 0.7152, 0.0722, //
100194
0.0193, 0.1192, 0.9503, //
101-
],
195+
]),
102196
gamma: GammaCorrection::SRGB,
103197
};
104198

105199
/// Adobe RGB color space
106200
pub const ADOBE: ColorSpace = ColorSpace {
107-
rgb: [
201+
rgb: Matrix3([
108202
2.0416, -0.5652, -0.3447, //
109203
-0.9695, 1.8763, 0.0415, //
110204
0.0135, -0.1184, 1.0154, //
111-
],
112-
xyz: [
205+
]),
206+
xyz: Matrix3([
113207
0.5767, 0.1856, 0.1882, //
114208
0.2974, 0.6273, 0.0753, //
115209
0.0270, 0.0707, 0.9911, //
116-
],
210+
]),
117211
gamma: GammaCorrection::NONE,
118212
};
213+
214+
#[cfg(test)]
215+
mod tests {
216+
use std::iter::zip;
217+
218+
use crate::colorspace::{ColorSpace, ADOBE, SRGB, WIDE};
219+
220+
macro_rules! compare {
221+
($expr:expr, $value:expr) => {
222+
let a = $expr;
223+
let b = $value;
224+
eprintln!("{a} vs {b:.4}");
225+
assert!((a - b).abs() < 1e-4);
226+
};
227+
}
228+
229+
fn verify_matrix(cs: &ColorSpace) {
230+
let xyz = &cs.xyz;
231+
let rgb = &cs.rgb;
232+
233+
let xyzi = xyz.inverted().unwrap();
234+
let rgbi = rgb.inverted().unwrap();
235+
236+
zip(xyz.0, rgbi.0).for_each(|(a, b)| {
237+
compare!(a, b);
238+
});
239+
240+
zip(rgb.0, xyzi.0).for_each(|(a, b)| {
241+
compare!(a, b);
242+
});
243+
}
244+
245+
#[test]
246+
fn iverse_wide() {
247+
verify_matrix(&WIDE);
248+
}
249+
250+
#[test]
251+
fn iverse_srgb() {
252+
verify_matrix(&SRGB);
253+
}
254+
255+
#[test]
256+
fn iverse_adobe() {
257+
verify_matrix(&ADOBE);
258+
}
259+
}

crates/hue/src/error.rs

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ pub enum HueError {
1515
#[error(transparent)]
1616
PackedStructError(#[from] packed_struct::PackingError),
1717

18+
#[error(transparent)]
19+
UuidError(#[from] uuid::Error),
20+
21+
#[error("Bad header in hue entertainment stream")]
22+
HueEntertainmentBadHeader,
23+
1824
#[error("Failed to decode Hue Zigbee Update")]
1925
HueZigbeeDecodeError,
2026

crates/hue/src/gamma.rs

+44
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,47 @@ impl GammaCorrection {
5959
/// Standard gamma correction for sRGB color space
6060
pub const SRGB: Self = Self::new(0.42, 0.003_130_8, 12.92, 0.055);
6161
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
use crate::gamma::GammaCorrection;
66+
67+
macro_rules! compare {
68+
($expr:expr, $value:expr) => {
69+
assert!(($expr - $value).abs() < 1e-5);
70+
};
71+
}
72+
73+
#[test]
74+
fn gamma_none() {
75+
let gc = GammaCorrection::NONE;
76+
77+
compare!(gc.transform(0.0), 0.0);
78+
compare!(gc.transform(0.1), 0.1);
79+
compare!(gc.transform(0.9), 0.9);
80+
compare!(gc.transform(1.0), 1.0);
81+
compare!(gc.transform(10.0), 10.0);
82+
}
83+
84+
#[test]
85+
fn inv_gamma_none() {
86+
let gc = GammaCorrection::NONE;
87+
88+
compare!(gc.inverse(0.0), 0.0);
89+
compare!(gc.inverse(0.1), 0.1);
90+
compare!(gc.inverse(0.9), 0.9);
91+
compare!(gc.inverse(1.0), 1.0);
92+
compare!(gc.inverse(10.0), 10.0);
93+
}
94+
95+
#[test]
96+
fn srgb_roundtrip() {
97+
let gc = GammaCorrection::SRGB;
98+
99+
compare!(gc.inverse(gc.transform(0.0)), 0.0);
100+
compare!(gc.inverse(gc.transform(0.1)), 0.1);
101+
compare!(gc.inverse(gc.transform(0.9)), 0.9);
102+
compare!(gc.inverse(gc.transform(1.0)), 1.0);
103+
compare!(gc.inverse(gc.transform(10.0)), 10.0);
104+
}
105+
}

crates/hue/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod colorspace;
55
pub mod error;
66
pub mod flags;
77
pub mod gamma;
8+
pub mod stream;
89
pub mod xy;
910
pub mod zigbee;
1011

0 commit comments

Comments
 (0)