mirror of
https://gitlab.com/asus-linux/asusctl.git
synced 2026-02-06 00:15:04 +01:00
rog-aura: Add flicker effect
This commit is contained in:
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Support for per-key config has been added to `asusd-user`. At the moment it is
|
- Support for per-key config has been added to `asusd-user`. At the moment it is
|
||||||
basic with only two effects done. Please see the manual for more information.
|
basic with only two effects done. Please see the manual for more information.
|
||||||
- Support for per-zone effects on some laptops. As above.
|
- Support for per-zone effects on some laptops. As above.
|
||||||
|
- Added three effects to use with Zoned or Per-Key:
|
||||||
|
+ Static, Breathe, Flicker. More to come.
|
||||||
### Changed
|
### Changed
|
||||||
- Create new rog-platform crate to manage all i/o in a universal way
|
- Create new rog-platform crate to manage all i/o in a universal way
|
||||||
+ kbd-led handling (requires kernel patches, TUF specific)
|
+ kbd-led handling (requires kernel patches, TUF specific)
|
||||||
|
|||||||
61
MANUAL.md
61
MANUAL.md
@@ -139,38 +139,51 @@ An Aura config itself is a file with contents:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
[
|
"name": "aura-default",
|
||||||
|
"aura": [
|
||||||
{
|
{
|
||||||
"led_type": {
|
"Breathe": {
|
||||||
"Key": "W"
|
"led_type": {
|
||||||
},
|
"Key": "W"
|
||||||
"action": {
|
},
|
||||||
"Breathe": {
|
"start_colour1": [
|
||||||
"colour1": [
|
255,
|
||||||
255,
|
0,
|
||||||
127,
|
20
|
||||||
0
|
],
|
||||||
],
|
"start_colour2": [
|
||||||
"colour2": [
|
20,
|
||||||
127,
|
255,
|
||||||
0,
|
0
|
||||||
255
|
],
|
||||||
],
|
"speed": "Low"
|
||||||
"speed": "Med"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"led_type": {
|
"Static": {
|
||||||
"Key": "Esc"
|
"led_type": {
|
||||||
},
|
"Key": "Esc"
|
||||||
"action": {
|
},
|
||||||
"Static": [
|
"colour": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
255
|
255
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Flicker": {
|
||||||
|
"led_type": {
|
||||||
|
"Key": "N9"
|
||||||
|
},
|
||||||
|
"colour": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"max_percentage": 80,
|
||||||
|
"min_percentage": 40
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -189,7 +202,7 @@ If your laptop supports multizone, `"led_type"` can also be `"PerZone": <one of
|
|||||||
- `"LightbarLeftCorner"`
|
- `"LightbarLeftCorner"`
|
||||||
- `"LightbarLeft"`
|
- `"LightbarLeft"`
|
||||||
|
|
||||||
At the moment there are only two effects available as shown in the example. More will come in the future
|
At the moment there are only three effects available as shown in the example. More will come in the future
|
||||||
but this may take me some time.
|
but this may take me some time.
|
||||||
|
|
||||||
**Aura layouts**: `asusd-user` does its best to find a suitable layout to use based on `/sys/class/dmi/id/board_name`.
|
**Aura layouts**: `asusd-user` does its best to find a suitable layout to use based on `/sys/class/dmi/id/board_name`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Using a combination of key-colour array plus a key layout to generate outputs.
|
//! Using a combination of key-colour array plus a key layout to generate outputs.
|
||||||
|
|
||||||
use rog_aura::{keys::Key, layouts::KeyLayout, ActionData, Colour, LedType, Sequences, Speed};
|
use rog_aura::{keys::Key, layouts::KeyLayout, Colour, LedType, Sequences, Speed, Effect, Breathe};
|
||||||
use rog_dbus::RogDbusClientBlocking;
|
use rog_dbus::RogDbusClientBlocking;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -9,12 +9,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let (client, _) = RogDbusClientBlocking::new().unwrap();
|
let (client, _) = RogDbusClientBlocking::new().unwrap();
|
||||||
|
|
||||||
let mut seq = Sequences::new();
|
let mut seq = Sequences::new();
|
||||||
let mut key = ActionData::new_breathe(
|
let mut key = Effect::Breathe(
|
||||||
LedType::Key(Key::W),
|
Breathe::new(LedType::Key(Key::W),
|
||||||
Colour(255, 127, 0),
|
Colour(255, 127, 0),
|
||||||
Colour(127, 0, 255),
|
Colour(127, 0, 255),
|
||||||
Speed::Med,
|
Speed::Med,
|
||||||
);
|
));
|
||||||
|
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::A));
|
key.set_led_type(LedType::Key(Key::A));
|
||||||
@@ -24,22 +24,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
key.set_led_type(LedType::Key(Key::D));
|
key.set_led_type(LedType::Key(Key::D));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
|
|
||||||
let mut key = ActionData::new_breathe(
|
let mut key = Effect::Breathe(
|
||||||
|
Breathe::new(
|
||||||
LedType::Key(Key::Q),
|
LedType::Key(Key::Q),
|
||||||
Colour(127, 127, 127),
|
Colour(127, 127, 127),
|
||||||
Colour(127, 255, 255),
|
Colour(127, 255, 255),
|
||||||
Speed::Low,
|
Speed::Low,
|
||||||
);
|
));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::E));
|
key.set_led_type(LedType::Key(Key::E));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
|
|
||||||
let mut key = ActionData::new_breathe(
|
let mut key = Effect::Breathe(
|
||||||
|
Breathe::new(
|
||||||
LedType::Key(Key::N1),
|
LedType::Key(Key::N1),
|
||||||
Colour(166, 127, 166),
|
Colour(166, 127, 166),
|
||||||
Colour(127, 155, 20),
|
Colour(127, 155, 20),
|
||||||
Speed::High,
|
Speed::High,
|
||||||
);
|
));
|
||||||
key.set_led_type(LedType::Key(Key::Tilde));
|
key.set_led_type(LedType::Key(Key::Tilde));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::N2));
|
key.set_led_type(LedType::Key(Key::N2));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Using a combination of key-colour array plus a key layout to generate outputs.
|
//! Using a combination of key-colour array plus a key layout to generate outputs.
|
||||||
|
|
||||||
use rog_aura::{layouts::KeyLayout, ActionData, Colour, LedType, PerZone, Sequences, Speed};
|
use rog_aura::{layouts::KeyLayout, Colour, LedType, PerZone, Sequences, Speed, Effect, Breathe};
|
||||||
use rog_dbus::RogDbusClientBlocking;
|
use rog_dbus::RogDbusClientBlocking;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -10,28 +10,31 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let mut seq = Sequences::new();
|
let mut seq = Sequences::new();
|
||||||
|
|
||||||
let zone = ActionData::new_breathe(
|
let zone = Effect::Breathe(
|
||||||
|
Breathe::new(
|
||||||
LedType::Zone(PerZone::KeyboardLeft),
|
LedType::Zone(PerZone::KeyboardLeft),
|
||||||
Colour(166, 127, 166),
|
Colour(166, 127, 166),
|
||||||
Colour(127, 155, 20),
|
Colour(127, 155, 20),
|
||||||
Speed::High,
|
Speed::High,
|
||||||
);
|
));
|
||||||
seq.push(zone);
|
seq.push(zone);
|
||||||
|
|
||||||
let zone = ActionData::new_breathe(
|
let zone = Effect::Breathe(
|
||||||
|
Breathe::new(
|
||||||
LedType::Zone(PerZone::KeyboardCenterLeft),
|
LedType::Zone(PerZone::KeyboardCenterLeft),
|
||||||
Colour(16, 127, 255),
|
Colour(16, 127, 255),
|
||||||
Colour(127, 15, 20),
|
Colour(127, 15, 20),
|
||||||
Speed::Low,
|
Speed::Low,
|
||||||
);
|
));
|
||||||
seq.push(zone);
|
seq.push(zone);
|
||||||
|
|
||||||
let zone = ActionData::new_breathe(
|
let zone = Effect::Breathe(
|
||||||
|
Breathe::new(
|
||||||
LedType::Zone(PerZone::LightbarRightCorner),
|
LedType::Zone(PerZone::LightbarRightCorner),
|
||||||
Colour(0, 255, 255),
|
Colour(0, 255, 255),
|
||||||
Colour(255, 0, 255),
|
Colour(255, 0, 255),
|
||||||
Speed::Med,
|
Speed::Med,
|
||||||
);
|
));
|
||||||
seq.push(zone);
|
seq.push(zone);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use rog_anime::{ActionLoader, AnimTime, AnimeType, Fade, Sequences, Vec2};
|
use rog_anime::{ActionLoader, AnimTime, AnimeType, Fade, Sequences, Vec2};
|
||||||
use rog_aura::{keys::Key, Colour, LedType, Speed};
|
use rog_aura::{keys::Key, Breathe, Colour, Effect, LedType, Speed, Static, Flicker};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
pub trait ConfigLoadSave<T> {
|
pub trait ConfigLoadSave<T: DeserializeOwned + serde::Serialize> {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
|
|
||||||
fn default_with_name(name: String) -> T;
|
fn default_with_name(name: String) -> T;
|
||||||
@@ -44,10 +44,7 @@ pub trait ConfigLoadSave<T> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(name: String) -> Result<T, Error>
|
fn load(name: String) -> Result<T, Error> {
|
||||||
where
|
|
||||||
T: DeserializeOwned + serde::Serialize,
|
|
||||||
{
|
|
||||||
let mut path = if let Some(dir) = dirs::config_dir() {
|
let mut path = if let Some(dir) = dirs::config_dir() {
|
||||||
dir
|
dir
|
||||||
} else {
|
} else {
|
||||||
@@ -197,12 +194,12 @@ impl ConfigLoadSave<UserAuraConfig> for UserAuraConfig {
|
|||||||
impl Default for UserAuraConfig {
|
impl Default for UserAuraConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut seq = rog_aura::Sequences::new();
|
let mut seq = rog_aura::Sequences::new();
|
||||||
let mut key = rog_aura::ActionData::new_breathe(
|
let mut key = Effect::Breathe(Breathe::new(
|
||||||
LedType::Key(Key::W),
|
LedType::Key(Key::W),
|
||||||
Colour(255, 0, 20),
|
Colour(255, 0, 20),
|
||||||
Colour(20, 255, 0),
|
Colour(20, 255, 0),
|
||||||
Speed::Low,
|
Speed::Low,
|
||||||
);
|
));
|
||||||
|
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::A));
|
key.set_led_type(LedType::Key(Key::A));
|
||||||
@@ -212,21 +209,24 @@ impl Default for UserAuraConfig {
|
|||||||
key.set_led_type(LedType::Key(Key::D));
|
key.set_led_type(LedType::Key(Key::D));
|
||||||
seq.push(key);
|
seq.push(key);
|
||||||
|
|
||||||
let key = rog_aura::ActionData::new_breathe(
|
let key = Effect::Breathe(Breathe::new(
|
||||||
LedType::Key(Key::F),
|
LedType::Key(Key::F),
|
||||||
Colour(255, 0, 0),
|
Colour(255, 0, 0),
|
||||||
Colour(255, 0, 0),
|
Colour(255, 0, 0),
|
||||||
Speed::High,
|
Speed::High,
|
||||||
);
|
));
|
||||||
seq.push(key);
|
seq.push(key);
|
||||||
|
|
||||||
let mut key = rog_aura::ActionData::new_static(LedType::Key(Key::RCtrl), Colour(0, 0, 255));
|
let mut key = Effect::Static(Static::new(LedType::Key(Key::RCtrl), Colour(0, 0, 255)));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::LCtrl));
|
key.set_led_type(LedType::Key(Key::LCtrl));
|
||||||
seq.push(key.clone());
|
seq.push(key.clone());
|
||||||
key.set_led_type(LedType::Key(Key::Esc));
|
key.set_led_type(LedType::Key(Key::Esc));
|
||||||
seq.push(key);
|
seq.push(key);
|
||||||
|
|
||||||
|
let key = Effect::Flicker(Flicker::new(LedType::Key(Key::N9), Colour(0, 0, 255), 80, 40));
|
||||||
|
seq.push(key.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "default".to_string(),
|
name: "default".to_string(),
|
||||||
aura: seq,
|
aura: seq,
|
||||||
|
|||||||
@@ -1,262 +0,0 @@
|
|||||||
use serde_derive::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
keys::Key, layouts::KeyLayout, Colour, KeyColourArray, PerKeyRaw, PerZone, Speed,
|
|
||||||
ZonedColourArray,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub enum LedType {
|
|
||||||
Key(Key),
|
|
||||||
Zone(PerZone),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LedType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Zone(PerZone::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub(super) enum Action {
|
|
||||||
Static(Colour),
|
|
||||||
Breathe {
|
|
||||||
/// The starting colour
|
|
||||||
colour1: Colour,
|
|
||||||
/// The secondary starting colour
|
|
||||||
colour2: Colour,
|
|
||||||
/// The speed at which to cycle between the colours
|
|
||||||
speed: Speed,
|
|
||||||
/// Temporary data to help keep state
|
|
||||||
#[serde(skip)]
|
|
||||||
colour_actual: Colour,
|
|
||||||
#[serde(skip)]
|
|
||||||
count_flipped: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
use_colour1: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Action {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Static(Colour::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ActionData {
|
|
||||||
led_type: LedType,
|
|
||||||
action: Action,
|
|
||||||
// TODO: time
|
|
||||||
/// The end resulting colour after stepping through effect
|
|
||||||
#[serde(skip)]
|
|
||||||
colour: Colour,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActionData {
|
|
||||||
pub fn set_led_type(&mut self, led_type: LedType) {
|
|
||||||
self.led_type = led_type
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_static(led_type: LedType, colour: Colour) -> Self {
|
|
||||||
Self {
|
|
||||||
led_type,
|
|
||||||
action: Action::Static(colour),
|
|
||||||
colour: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_breathe(led_type: LedType, colour1: Colour, colour2: Colour, speed: Speed) -> Self {
|
|
||||||
Self {
|
|
||||||
led_type,
|
|
||||||
action: Action::Breathe {
|
|
||||||
colour1,
|
|
||||||
colour2,
|
|
||||||
speed,
|
|
||||||
colour_actual: colour1,
|
|
||||||
count_flipped: false,
|
|
||||||
use_colour1: true,
|
|
||||||
},
|
|
||||||
colour: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_state(&mut self, _layout: &KeyLayout) {
|
|
||||||
match &mut self.action {
|
|
||||||
Action::Static(c) => self.colour = *c,
|
|
||||||
Action::Breathe {
|
|
||||||
colour1,
|
|
||||||
colour2,
|
|
||||||
speed,
|
|
||||||
colour_actual,
|
|
||||||
count_flipped: flipped,
|
|
||||||
use_colour1,
|
|
||||||
} => {
|
|
||||||
let speed = 4 - <u8>::from(*speed);
|
|
||||||
|
|
||||||
let colour: &mut Colour;
|
|
||||||
if *colour_actual == Colour(0, 0, 0) {
|
|
||||||
*use_colour1 = !*use_colour1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*use_colour1 {
|
|
||||||
colour = colour2;
|
|
||||||
} else {
|
|
||||||
colour = colour1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let r1_scale = colour.0 / speed / 2;
|
|
||||||
let g1_scale = colour.1 / speed / 2;
|
|
||||||
let b1_scale = colour.2 / speed / 2;
|
|
||||||
|
|
||||||
if *colour_actual == Colour(0, 0, 0) {
|
|
||||||
*flipped = true;
|
|
||||||
} else if colour_actual >= colour {
|
|
||||||
*flipped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*flipped {
|
|
||||||
colour_actual.0 = colour_actual.0.saturating_sub(r1_scale);
|
|
||||||
colour_actual.1 = colour_actual.1.saturating_sub(g1_scale);
|
|
||||||
colour_actual.2 = colour_actual.2.saturating_sub(b1_scale);
|
|
||||||
} else {
|
|
||||||
colour_actual.0 = colour_actual.0.saturating_add(r1_scale);
|
|
||||||
colour_actual.1 = colour_actual.1.saturating_add(g1_scale);
|
|
||||||
colour_actual.2 = colour_actual.2.saturating_add(b1_scale);
|
|
||||||
}
|
|
||||||
self.colour = *colour_actual;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
|
||||||
pub struct Sequences(Vec<ActionData>);
|
|
||||||
|
|
||||||
impl Sequences {
|
|
||||||
#[inline]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn push(&mut self, action: ActionData) {
|
|
||||||
self.0.push(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn insert(&mut self, index: usize, action: ActionData) {
|
|
||||||
self.0.insert(index, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove an item at this position from the run buffer. If the `index` supplied
|
|
||||||
/// is not in range then `None` is returned, otherwise the `ActionData` at that location
|
|
||||||
/// is yeeted and returned.
|
|
||||||
#[inline]
|
|
||||||
pub fn remove_item(&mut self, index: usize) -> Option<ActionData> {
|
|
||||||
if index < self.0.len() {
|
|
||||||
return Some(self.0.remove(index));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_state(&mut self, layout: &KeyLayout) {
|
|
||||||
for effect in self.0.iter_mut() {
|
|
||||||
effect.next_state(layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_packets(&self) -> PerKeyRaw {
|
|
||||||
let mut keys = KeyColourArray::new();
|
|
||||||
let mut zones = ZonedColourArray::new();
|
|
||||||
let mut is_per_key = false;
|
|
||||||
for effect in self.0.iter() {
|
|
||||||
match effect.led_type {
|
|
||||||
LedType::Key(key) => {
|
|
||||||
is_per_key = true;
|
|
||||||
if let Some(rgb) = keys.rgb_for_key(key) {
|
|
||||||
rgb[0] = effect.colour.0;
|
|
||||||
rgb[1] = effect.colour.1;
|
|
||||||
rgb[2] = effect.colour.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LedType::Zone(z) => {
|
|
||||||
let rgb = zones.rgb_for_zone(z);
|
|
||||||
rgb[0] = effect.colour.0;
|
|
||||||
rgb[1] = effect.colour.1;
|
|
||||||
rgb[2] = effect.colour.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_per_key {
|
|
||||||
keys.into()
|
|
||||||
} else {
|
|
||||||
vec![zones.into()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{
|
|
||||||
keys::Key, layouts::KeyLayout, Action, ActionData, Colour, LedType, Sequences, Speed,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_key_next_state_then_create() {
|
|
||||||
let layout = KeyLayout::gx502_layout();
|
|
||||||
let mut seq = Sequences::new();
|
|
||||||
seq.0.push(ActionData {
|
|
||||||
led_type: LedType::Key(Key::F),
|
|
||||||
action: Action::Static(Colour(255, 127, 0)),
|
|
||||||
colour: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
seq.next_state(&layout);
|
|
||||||
let packets = seq.create_packets();
|
|
||||||
|
|
||||||
assert_eq!(packets[0][0], 0x5d);
|
|
||||||
assert_eq!(packets[5][33], 255);
|
|
||||||
assert_eq!(packets[5][34], 127);
|
|
||||||
assert_eq!(packets[5][35], 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cycle_breathe() {
|
|
||||||
let layout = KeyLayout::gx502_layout();
|
|
||||||
let mut seq = Sequences::new();
|
|
||||||
seq.0.push(ActionData {
|
|
||||||
led_type: LedType::Key(Key::F),
|
|
||||||
action: Action::Breathe {
|
|
||||||
colour1: Colour(255, 127, 0),
|
|
||||||
colour2: Colour(127, 0, 255),
|
|
||||||
speed: Speed::Med,
|
|
||||||
colour_actual: Colour(255, 127, 0),
|
|
||||||
count_flipped: false,
|
|
||||||
use_colour1: true,
|
|
||||||
},
|
|
||||||
colour: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let s = serde_json::to_string_pretty(&seq).unwrap();
|
|
||||||
println!("{s}");
|
|
||||||
|
|
||||||
seq.next_state(&layout);
|
|
||||||
let packets = seq.create_packets();
|
|
||||||
|
|
||||||
assert_eq!(packets[0][0], 0x5d);
|
|
||||||
assert_eq!(packets[5][33], 213);
|
|
||||||
assert_eq!(packets[5][34], 106);
|
|
||||||
assert_eq!(packets[5][35], 0);
|
|
||||||
|
|
||||||
// dbg!(&packets[5][33..=35]);
|
|
||||||
|
|
||||||
seq.next_state(&layout);
|
|
||||||
let packets = seq.create_packets();
|
|
||||||
|
|
||||||
assert_eq!(packets[0][0], 0x5d);
|
|
||||||
assert_eq!(packets[5][33], 171);
|
|
||||||
assert_eq!(packets[5][34], 85);
|
|
||||||
assert_eq!(packets[5][35], 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
249
rog-aura/src/sequencer/effects.rs
Normal file
249
rog-aura/src/sequencer/effects.rs
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
use crate::{layouts::KeyLayout, p_random, Colour, EffectState, LedType, Speed};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
macro_rules! effect_state_impl {
|
||||||
|
() => {
|
||||||
|
fn get_colour(&self) -> Colour {
|
||||||
|
self.colour
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_led_type(&self) -> LedType {
|
||||||
|
self.led_type.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the led type
|
||||||
|
fn set_led_type(&mut self, led_type: LedType) {
|
||||||
|
self.led_type = led_type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! effect_impl {
|
||||||
|
($($effect:ident),*) => {
|
||||||
|
impl Effect {
|
||||||
|
/// Get the type of LED set
|
||||||
|
pub fn get_led_type(&self) -> LedType {
|
||||||
|
match self {
|
||||||
|
$(Effect::$effect(c) => c.get_led_type(),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the led type
|
||||||
|
pub fn set_led_type(&mut self, led_type: LedType) {
|
||||||
|
match self {
|
||||||
|
$(Effect::$effect(c) => c.set_led_type(led_type),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the next state of the effect
|
||||||
|
pub fn next_state(&mut self, layout: &KeyLayout) {
|
||||||
|
match self {
|
||||||
|
$(Effect::$effect(c) => c.next_colour_state(layout),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the calculated colour
|
||||||
|
pub fn get_colour(&self) -> Colour {
|
||||||
|
match self {
|
||||||
|
$(Effect::$effect(c) => c.get_colour(),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************************************************************************/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub enum Effect {
|
||||||
|
Static(Static),
|
||||||
|
Breathe(Breathe),
|
||||||
|
Flicker(Flicker),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Effect {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Static(Static::new(LedType::default(), Colour::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effect_impl!(Static, Breathe, Flicker);
|
||||||
|
|
||||||
|
/**************************************************************************************************/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Static {
|
||||||
|
led_type: LedType,
|
||||||
|
/// The starting colour
|
||||||
|
colour: Colour,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Static {
|
||||||
|
pub fn new(led_type: LedType, colour: Colour) -> Self {
|
||||||
|
Self { led_type, colour }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectState for Static {
|
||||||
|
fn next_colour_state(&mut self, _layout: &KeyLayout) {}
|
||||||
|
|
||||||
|
effect_state_impl!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************************************************************************/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Breathe {
|
||||||
|
led_type: LedType,
|
||||||
|
/// The starting colour
|
||||||
|
start_colour1: Colour,
|
||||||
|
/// The secondary starting colour
|
||||||
|
start_colour2: Colour,
|
||||||
|
/// The speed at which to cycle between the colours
|
||||||
|
speed: Speed,
|
||||||
|
/// Temporary data to help keep state
|
||||||
|
#[serde(skip)]
|
||||||
|
colour: Colour,
|
||||||
|
#[serde(skip)]
|
||||||
|
count_flipped: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
use_colour1: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Breathe {
|
||||||
|
pub fn new(led_type: LedType, colour1: Colour, colour2: Colour, speed: Speed) -> Self {
|
||||||
|
Self {
|
||||||
|
led_type,
|
||||||
|
start_colour1: colour1,
|
||||||
|
start_colour2: colour2,
|
||||||
|
speed,
|
||||||
|
colour: colour1,
|
||||||
|
count_flipped: false,
|
||||||
|
use_colour1: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectState for Breathe {
|
||||||
|
fn next_colour_state(&mut self, _layout: &KeyLayout) {
|
||||||
|
let Self {
|
||||||
|
start_colour1: colour1,
|
||||||
|
start_colour2: colour2,
|
||||||
|
speed,
|
||||||
|
colour: colour_actual,
|
||||||
|
count_flipped: flipped,
|
||||||
|
use_colour1,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let speed = 4 - <u8>::from(*speed);
|
||||||
|
|
||||||
|
let colour: &mut Colour;
|
||||||
|
if *colour_actual == Colour(0, 0, 0) {
|
||||||
|
*use_colour1 = !*use_colour1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*use_colour1 {
|
||||||
|
colour = colour2;
|
||||||
|
} else {
|
||||||
|
colour = colour1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r1_scale = colour.0 / speed / 2;
|
||||||
|
let g1_scale = colour.1 / speed / 2;
|
||||||
|
let b1_scale = colour.2 / speed / 2;
|
||||||
|
|
||||||
|
if *colour_actual == Colour(0, 0, 0) {
|
||||||
|
*flipped = true;
|
||||||
|
} else if colour_actual >= colour {
|
||||||
|
*flipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*flipped {
|
||||||
|
colour_actual.0 = colour_actual.0.saturating_sub(r1_scale);
|
||||||
|
colour_actual.1 = colour_actual.1.saturating_sub(g1_scale);
|
||||||
|
colour_actual.2 = colour_actual.2.saturating_sub(b1_scale);
|
||||||
|
} else {
|
||||||
|
colour_actual.0 = colour_actual.0.saturating_add(r1_scale);
|
||||||
|
colour_actual.1 = colour_actual.1.saturating_add(g1_scale);
|
||||||
|
colour_actual.2 = colour_actual.2.saturating_add(b1_scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effect_state_impl!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************************************************************************/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Flicker {
|
||||||
|
led_type: LedType,
|
||||||
|
/// The starting colour
|
||||||
|
colour: Colour,
|
||||||
|
max_percentage: u8,
|
||||||
|
min_percentage: u8,
|
||||||
|
#[serde(skip)]
|
||||||
|
count: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Flicker {
|
||||||
|
pub fn new(led_type: LedType, colour: Colour, max_percentage: u8, min_percentage: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
led_type,
|
||||||
|
colour,
|
||||||
|
count: 4,
|
||||||
|
max_percentage,
|
||||||
|
min_percentage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectState for Flicker {
|
||||||
|
fn next_colour_state(&mut self, _layout: &KeyLayout) {
|
||||||
|
let Self {
|
||||||
|
max_percentage,
|
||||||
|
min_percentage,
|
||||||
|
colour,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
self.count -= 1;
|
||||||
|
if self.count != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make a "percentage" method on Colour.
|
||||||
|
let max_light = Colour(
|
||||||
|
(colour.0 as f32 / 100.0 * *max_percentage as f32) as u8,
|
||||||
|
(colour.1 as f32 / 100.0 * *max_percentage as f32) as u8,
|
||||||
|
(colour.2 as f32 / 100.0 * *max_percentage as f32) as u8,
|
||||||
|
);
|
||||||
|
// min light is a percentage of the set colour
|
||||||
|
let min_light = Colour(
|
||||||
|
(colour.0 as f32 / 100.0 * *min_percentage as f32) as u8,
|
||||||
|
(colour.1 as f32 / 100.0 * *min_percentage as f32) as u8,
|
||||||
|
(colour.2 as f32 / 100.0 * *min_percentage as f32) as u8,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert the 255 to percentage
|
||||||
|
let amount = (p_random() & 7) as f32 * 8.0;
|
||||||
|
|
||||||
|
let set_colour = |colour: &mut u8, max: f32, min: f32| {
|
||||||
|
let pc = amount / max * 100.0;
|
||||||
|
let min_amount = pc * min / 100.0; // percentage of min colour
|
||||||
|
let max_amount = pc * max / 100.0; // percentage of max colour
|
||||||
|
if *colour as f32 - min_amount < min {
|
||||||
|
*colour = min as u8;
|
||||||
|
} else {
|
||||||
|
*colour = (max - max_amount) as u8;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
set_colour(&mut colour.0, max_light.0 as f32, min_light.0 as f32);
|
||||||
|
set_colour(&mut colour.1, max_light.1 as f32, min_light.1 as f32);
|
||||||
|
set_colour(&mut colour.2, max_light.2 as f32, min_light.2 as f32);
|
||||||
|
|
||||||
|
self.count = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
effect_state_impl!();
|
||||||
|
}
|
||||||
218
rog-aura/src/sequencer/mod.rs
Normal file
218
rog-aura/src/sequencer/mod.rs
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
mod effects;
|
||||||
|
pub use effects::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
keys::Key, layouts::KeyLayout, Colour, KeyColourArray, PerKeyRaw, PerZone, ZonedColourArray,
|
||||||
|
};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// static mut RNDINDEX: usize = 0;
|
||||||
|
static mut PRNDINDEX: usize = 0;
|
||||||
|
|
||||||
|
pub const RNDTABLE: [i32; 256] = [
|
||||||
|
0, 8, 109, 220, 222, 241, 149, 107, 75, 248, 254, 140, 16, 66, 74, 21, 211, 47, 80, 242, 154,
|
||||||
|
27, 205, 128, 161, 89, 77, 36, 95, 110, 85, 48, 212, 140, 211, 249, 22, 79, 200, 50, 28, 188,
|
||||||
|
52, 140, 202, 120, 68, 145, 62, 70, 184, 190, 91, 197, 152, 224, 149, 104, 25, 178, 252, 182,
|
||||||
|
202, 182, 141, 197, 4, 81, 181, 242, 145, 42, 39, 227, 156, 198, 225, 193, 219, 93, 122, 175,
|
||||||
|
249, 0, 175, 143, 70, 239, 46, 246, 163, 53, 163, 109, 168, 135, 2, 235, 25, 92, 20, 145, 138,
|
||||||
|
77, 69, 166, 78, 176, 173, 212, 166, 113, 94, 161, 41, 50, 239, 49, 111, 164, 70, 60, 2, 37,
|
||||||
|
171, 75, 136, 156, 11, 56, 42, 146, 138, 229, 73, 146, 77, 61, 98, 196, 135, 106, 63, 197, 195,
|
||||||
|
86, 96, 203, 113, 101, 170, 247, 181, 113, 80, 250, 108, 7, 255, 237, 129, 226, 79, 107, 112,
|
||||||
|
166, 103, 241, 24, 223, 239, 120, 198, 58, 60, 82, 128, 3, 184, 66, 143, 224, 145, 224, 81,
|
||||||
|
206, 163, 45, 63, 90, 168, 114, 59, 33, 159, 95, 28, 139, 123, 98, 125, 196, 15, 70, 194, 253,
|
||||||
|
54, 14, 109, 226, 71, 17, 161, 93, 186, 87, 244, 138, 20, 52, 123, 251, 26, 36, 17, 46, 52,
|
||||||
|
231, 232, 76, 31, 221, 84, 37, 216, 165, 212, 106, 197, 242, 98, 43, 39, 175, 254, 145, 190,
|
||||||
|
84, 118, 222, 187, 136, 120, 163, 236, 249,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn p_random() -> i32 {
|
||||||
|
unsafe {
|
||||||
|
PRNDINDEX = (PRNDINDEX + 1) & 0xFF;
|
||||||
|
RNDTABLE[PRNDINDEX]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait EffectState {
|
||||||
|
/// Calculate the next colour state
|
||||||
|
fn next_colour_state(&mut self, _layout: &KeyLayout);
|
||||||
|
|
||||||
|
/// Return the resulting colour. Implementers should store the colour to return it.
|
||||||
|
fn get_colour(&self) -> Colour;
|
||||||
|
|
||||||
|
fn get_led_type(&self) -> LedType;
|
||||||
|
|
||||||
|
fn set_led_type(&mut self, led_type: LedType);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub enum LedType {
|
||||||
|
Key(Key),
|
||||||
|
Zone(PerZone),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LedType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Zone(PerZone::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||||
|
pub struct Sequences(Vec<Effect>);
|
||||||
|
|
||||||
|
impl Sequences {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn push(&mut self, action: Effect) {
|
||||||
|
self.0.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert(&mut self, index: usize, action: Effect) {
|
||||||
|
self.0.insert(index, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an item at this position from the run buffer. If the `index` supplied
|
||||||
|
/// is not in range then `None` is returned, otherwise the `ActionData` at that location
|
||||||
|
/// is yeeted and returned.
|
||||||
|
#[inline]
|
||||||
|
pub fn remove_item(&mut self, index: usize) -> Option<Effect> {
|
||||||
|
if index < self.0.len() {
|
||||||
|
return Some(self.0.remove(index));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_state(&mut self, layout: &KeyLayout) {
|
||||||
|
for effect in self.0.iter_mut() {
|
||||||
|
effect.next_state(layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_packets(&self) -> PerKeyRaw {
|
||||||
|
let mut keys = KeyColourArray::new();
|
||||||
|
let mut zones = ZonedColourArray::new();
|
||||||
|
let mut is_per_key = false;
|
||||||
|
for effect in self.0.iter() {
|
||||||
|
match effect.get_led_type() {
|
||||||
|
LedType::Key(key) => {
|
||||||
|
is_per_key = true;
|
||||||
|
if let Some(rgb) = keys.rgb_for_key(key) {
|
||||||
|
let c = effect.get_colour();
|
||||||
|
rgb[0] = c.0;
|
||||||
|
rgb[1] = c.1;
|
||||||
|
rgb[2] = c.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LedType::Zone(z) => {
|
||||||
|
let rgb = zones.rgb_for_zone(z);
|
||||||
|
let c = effect.get_colour();
|
||||||
|
rgb[0] = c.0;
|
||||||
|
rgb[1] = c.1;
|
||||||
|
rgb[2] = c.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_per_key {
|
||||||
|
keys.into()
|
||||||
|
} else {
|
||||||
|
vec![zones.into()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
keys::Key, layouts::KeyLayout, Breathe, Colour, Effect, Flicker, LedType, Sequences, Speed,
|
||||||
|
Static,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_key_next_state_then_create() {
|
||||||
|
let layout = KeyLayout::gx502_layout();
|
||||||
|
let mut seq = Sequences::new();
|
||||||
|
seq.0.push(Effect::Static(Static::new(
|
||||||
|
LedType::Key(Key::F),
|
||||||
|
Colour(255, 127, 0),
|
||||||
|
)));
|
||||||
|
|
||||||
|
seq.next_state(&layout);
|
||||||
|
let packets = seq.create_packets();
|
||||||
|
|
||||||
|
assert_eq!(packets[0][0], 0x5d);
|
||||||
|
assert_eq!(packets[5][33], 255);
|
||||||
|
assert_eq!(packets[5][34], 127);
|
||||||
|
assert_eq!(packets[5][35], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_breathe() {
|
||||||
|
let layout = KeyLayout::gx502_layout();
|
||||||
|
let mut seq = Sequences::new();
|
||||||
|
seq.0.push(Effect::Breathe(Breathe::new(
|
||||||
|
LedType::Key(Key::F),
|
||||||
|
Colour(255, 127, 0),
|
||||||
|
Colour(127, 0, 255),
|
||||||
|
Speed::Med,
|
||||||
|
)));
|
||||||
|
|
||||||
|
let s = serde_json::to_string_pretty(&seq).unwrap();
|
||||||
|
println!("{s}");
|
||||||
|
|
||||||
|
seq.next_state(&layout);
|
||||||
|
let packets = seq.create_packets();
|
||||||
|
|
||||||
|
assert_eq!(packets[0][0], 0x5d);
|
||||||
|
assert_eq!(packets[5][33], 213);
|
||||||
|
assert_eq!(packets[5][34], 106);
|
||||||
|
assert_eq!(packets[5][35], 0);
|
||||||
|
|
||||||
|
// dbg!(&packets[5][33..=35]);
|
||||||
|
|
||||||
|
seq.next_state(&layout);
|
||||||
|
let packets = seq.create_packets();
|
||||||
|
|
||||||
|
assert_eq!(packets[0][0], 0x5d);
|
||||||
|
assert_eq!(packets[5][33], 171);
|
||||||
|
assert_eq!(packets[5][34], 85);
|
||||||
|
assert_eq!(packets[5][35], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_flicker() {
|
||||||
|
let layout = KeyLayout::gx502_layout();
|
||||||
|
let mut seq = Sequences::new();
|
||||||
|
seq.0.push(Effect::Flicker(Flicker::new(
|
||||||
|
LedType::Key(Key::F),
|
||||||
|
Colour(255, 127, 80),
|
||||||
|
100,
|
||||||
|
10,
|
||||||
|
)));
|
||||||
|
|
||||||
|
seq.next_state(&layout);
|
||||||
|
let packets = seq.create_packets();
|
||||||
|
|
||||||
|
assert_eq!(packets[0][0], 0x5d);
|
||||||
|
assert_eq!(packets[5][33], 255);
|
||||||
|
assert_eq!(packets[5][34], 127);
|
||||||
|
assert_eq!(packets[5][35], 80);
|
||||||
|
|
||||||
|
// The random is deterministic
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
seq.next_state(&layout);
|
||||||
|
|
||||||
|
let packets = seq.create_packets();
|
||||||
|
assert_eq!(packets[5][33], 215);
|
||||||
|
assert_eq!(packets[5][34], 87);
|
||||||
|
assert_eq!(packets[5][35], 40);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user