mirror of
https://github.com/fossasia/badgemagic-rs
synced 2025-07-16 09:33:59 +00:00
Merge b344ac00b1
into 44222843d8
This commit is contained in:
commit
f5f5a8a237
9 changed files with 1210 additions and 143 deletions
1159
Cargo.lock
generated
1159
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
25
Cargo.toml
25
Cargo.toml
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "badgemagic"
|
name = "badgemagic"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
authors = ["Martin Michaelis <code@mgjm.de>"]
|
authors = ["Martin Michaelis <code@mgjm.de>", "Valentin Weber <weva+code@kabelsalat.ch>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Badge Magic with LEDs - Library and CLI"
|
description = "Badge Magic with LEDs - Library and CLI"
|
||||||
homepage = "https://badgemagic.fossasia.org"
|
homepage = "https://badgemagic.fossasia.org"
|
||||||
|
@ -35,18 +35,21 @@ embedded-graphics = ["dep:embedded-graphics"]
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
usb-hid = ["dep:hidapi"]
|
usb-hid = ["dep:hidapi"]
|
||||||
ble = ["dep:btleplug", "dep:uuid", "dep:tokio"]
|
ble = ["dep:btleplug", "dep:uuid", "dep:tokio"]
|
||||||
|
u8g2-fonts = ["dep:u8g2-fonts"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.98"
|
||||||
base64 = { version = "0.22.1", optional = true }
|
base64 = { version = "0.22.1", optional = true }
|
||||||
clap = { version = "4.5.23", features = ["derive"], optional = true }
|
clap = { version = "4.5.40", features = ["derive"], optional = true }
|
||||||
embedded-graphics = { version = "0.8.1", optional = true }
|
embedded-graphics = { version = "0.8.1", optional = true }
|
||||||
hidapi = { version = "2.6.3", optional = true }
|
hidapi = { version = "2.6.3", optional = true }
|
||||||
btleplug = { version = "0.11.6", optional = true }
|
btleplug = { version = "0.11.8", optional = true }
|
||||||
uuid = { version = "1.11.0", optional = true }
|
uuid = { version = "1.17.0", optional = true }
|
||||||
tokio = { version = "1.39.2", features = ["rt"], optional = true }
|
tokio = { version = "1.46.1", features = ["rt"], optional = true }
|
||||||
serde = { version = "1.0.217", features = ["derive"], optional = true }
|
serde = { version = "1.0.219", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1.0.134", optional = true }
|
serde_json = { version = "1.0.140", optional = true }
|
||||||
time = "0.3.37"
|
time = "0.3.41"
|
||||||
toml = { version = "0.8.19", optional = true }
|
toml = { version = "0.9.0", optional = true }
|
||||||
zerocopy = { version = "0.8.14", features = ["derive"] }
|
zerocopy = { version = "0.8.14", features = ["derive"] }
|
||||||
|
u8g2-fonts = { version = "0.7.1", features = ["embedded_graphics_textstyle"], optional = true }
|
||||||
|
image = "0.25.6"
|
|
@ -1,4 +1,5 @@
|
||||||
Copyright 2024 Martin Michaelis
|
Copyright 2024 Martin Michaelis
|
||||||
|
Copyright 2025 Valentin Weber
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the “Software”), to deal
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
|
|
@ -28,7 +28,7 @@ chmod +x badgemagic
|
||||||
./badgemagic --help
|
./badgemagic --help
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: The windows and macOS build is not actively tested. Please try it out and report back whether it worked or any problems that might occour.
|
> Note: The windows and macOS build is not actively tested. Please try it out and report back whether it worked or any problems that might occur.
|
||||||
|
|
||||||
If your system is not listed above (Linux / Windows on ARM, musl, etc.) or you want to do it anyway, you can install this tool from source:
|
If your system is not listed above (Linux / Windows on ARM, musl, etc.) or you want to do it anyway, you can install this tool from source:
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,13 @@ use badgemagic::{
|
||||||
embedded_graphics::{
|
embedded_graphics::{
|
||||||
geometry::Point, mono_font::MonoTextStyle, pixelcolor::BinaryColor, text::Text,
|
geometry::Point, mono_font::MonoTextStyle, pixelcolor::BinaryColor, text::Text,
|
||||||
},
|
},
|
||||||
protocol::{Mode, PayloadBuffer, Style},
|
protocol::{Brightness, Mode, PayloadBuffer, Style},
|
||||||
usb_hid::Device,
|
usb_hid::Device,
|
||||||
util::DrawableLayoutExt,
|
util::DrawableLayoutExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut payload = PayloadBuffer::new();
|
let mut payload = PayloadBuffer::new(Brightness::default());
|
||||||
|
|
||||||
payload.add_message_drawable(
|
payload.add_message_drawable(
|
||||||
Style::default().mode(Mode::Center),
|
Style::default().mode(Mode::Center),
|
||||||
|
|
|
@ -191,7 +191,7 @@ impl Device {
|
||||||
BLE_CHAR_CHUNK_SIZE
|
BLE_CHAR_CHUNK_SIZE
|
||||||
);
|
);
|
||||||
|
|
||||||
// the device will brick itself if the payload is too long (more then 8192 bytes)
|
// the device will brick itself if the payload is too long (more than 8192 bytes)
|
||||||
anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)");
|
anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)");
|
||||||
|
|
||||||
for chunk in data.chunks(BLE_CHAR_CHUNK_SIZE) {
|
for chunk in data.chunks(BLE_CHAR_CHUNK_SIZE) {
|
||||||
|
|
92
src/main.rs
92
src/main.rs
|
@ -1,24 +1,29 @@
|
||||||
#![warn(clippy::all, clippy::pedantic)]
|
#![warn(clippy::all, clippy::pedantic)]
|
||||||
|
|
||||||
use std::{fs, path::PathBuf};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use badgemagic::{
|
use badgemagic::{
|
||||||
ble::Device as BleDevice,
|
ble::Device as BleDevice,
|
||||||
protocol::{Mode, PayloadBuffer, Speed, Style},
|
protocol::{Brightness, Mode, PayloadBuffer, Speed, Style},
|
||||||
usb_hid::Device as UsbDevice,
|
usb_hid::Device as UsbDevice,
|
||||||
};
|
};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
#[cfg(not(any(feature = "u8g2-fonts")))]
|
||||||
|
use embedded_graphics::mono_font::{iso_8859_1::FONT_6X9, MonoTextStyle};
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
geometry::Point,
|
geometry::Point,
|
||||||
image::{Image, ImageRawLE},
|
image::{Image, ImageRawLE},
|
||||||
mono_font::{iso_8859_1::FONT_6X9, MonoTextStyle},
|
|
||||||
pixelcolor::BinaryColor,
|
pixelcolor::BinaryColor,
|
||||||
text::Text,
|
text::Text,
|
||||||
Drawable, Pixel,
|
Drawable, Pixel,
|
||||||
};
|
};
|
||||||
|
use image::{
|
||||||
|
codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, ImageReader, Pixel as iPixel,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::{fs, fs::File, io::BufReader, path::PathBuf};
|
||||||
|
#[cfg(feature = "u8g2-fonts")]
|
||||||
|
use u8g2_fonts::{fonts::u8g2_font_lucasfont_alternate_tf, U8g2TextStyle};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
/// Upload a configuration with up to 8 messages to an LED badge
|
/// Upload a configuration with up to 8 messages to an LED badge
|
||||||
|
@ -43,6 +48,10 @@ struct Args {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
transport: TransportProtocol,
|
transport: TransportProtocol,
|
||||||
|
|
||||||
|
/// Brightness of the panel
|
||||||
|
#[clap(long)]
|
||||||
|
brightness: Option<Brightness>,
|
||||||
|
|
||||||
/// List all devices visible to a transport and exit
|
/// List all devices visible to a transport and exit
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
list_devices: bool,
|
list_devices: bool,
|
||||||
|
@ -91,8 +100,8 @@ enum Content {
|
||||||
Bitstring { bitstring: String },
|
Bitstring { bitstring: String },
|
||||||
BitmapBase64 { width: u32, bitmap_base64: String },
|
BitmapBase64 { width: u32, bitmap_base64: String },
|
||||||
BitmapFile { width: u32, bitmap_file: PathBuf },
|
BitmapFile { width: u32, bitmap_file: PathBuf },
|
||||||
// TODO: implement png
|
ImageFile { img_file: PathBuf },
|
||||||
// PngFile { png_file: PathBuf },
|
GifFile { gif_file: PathBuf },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
@ -102,7 +111,7 @@ fn main() -> Result<()> {
|
||||||
return list_devices(&args.transport);
|
return list_devices(&args.transport);
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = gnerate_payload(&mut args)?;
|
let payload = generate_payload(&mut args)?;
|
||||||
|
|
||||||
write_payload(&args.transport, payload)
|
write_payload(&args.transport, payload)
|
||||||
}
|
}
|
||||||
|
@ -128,7 +137,7 @@ fn list_devices(transport: &TransportProtocol) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gnerate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
fn generate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
||||||
let config_path = args.config.take().unwrap_or_default();
|
let config_path = args.config.take().unwrap_or_default();
|
||||||
let config = fs::read_to_string(&config_path)
|
let config = fs::read_to_string(&config_path)
|
||||||
.with_context(|| format!("load config: {config_path:?}"))?;
|
.with_context(|| format!("load config: {config_path:?}"))?;
|
||||||
|
@ -146,7 +155,7 @@ fn gnerate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut payload = PayloadBuffer::new();
|
let mut payload = PayloadBuffer::new(args.brightness.unwrap_or_default());
|
||||||
|
|
||||||
for message in config.messages {
|
for message in config.messages {
|
||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
|
@ -157,13 +166,23 @@ fn gnerate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
||||||
style = style.border();
|
style = style.border();
|
||||||
}
|
}
|
||||||
style = style.speed(message.speed).mode(message.mode);
|
style = style.speed(message.speed).mode(message.mode);
|
||||||
|
|
||||||
match message.content {
|
match message.content {
|
||||||
Content::Text { text } => {
|
Content::Text { text } => {
|
||||||
|
#[cfg(not(any(feature = "u8g2-fonts")))]
|
||||||
let text = Text::new(
|
let text = Text::new(
|
||||||
&text,
|
&text,
|
||||||
Point::new(0, 7),
|
Point::new(0, 7),
|
||||||
MonoTextStyle::new(&FONT_6X9, BinaryColor::On),
|
MonoTextStyle::new(&FONT_6X9, BinaryColor::On),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "u8g2-fonts")]
|
||||||
|
let text = Text::new(
|
||||||
|
&text,
|
||||||
|
Point::new(0, 8),
|
||||||
|
U8g2TextStyle::new(u8g2_font_lucasfont_alternate_tf, BinaryColor::On),
|
||||||
|
);
|
||||||
|
|
||||||
payload.add_message_drawable(style, &text);
|
payload.add_message_drawable(style, &text);
|
||||||
}
|
}
|
||||||
Content::Bitstring { bitstring } => {
|
Content::Bitstring { bitstring } => {
|
||||||
|
@ -190,12 +209,8 @@ fn gnerate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
||||||
// off
|
// off
|
||||||
}
|
}
|
||||||
'X' => {
|
'X' => {
|
||||||
Pixel(
|
Pixel(Point::new(x.try_into()?, y.try_into()?), BinaryColor::On)
|
||||||
Point::new(x.try_into().unwrap(), y.try_into().unwrap()),
|
.draw(&mut buffer)?;
|
||||||
BinaryColor::On,
|
|
||||||
)
|
|
||||||
.draw(&mut buffer)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
_ => anyhow::bail!("invalid bit value for bit ({x}, {y}): {c:?}"),
|
_ => anyhow::bail!("invalid bit value for bit ({x}, {y}): {c:?}"),
|
||||||
}
|
}
|
||||||
|
@ -223,6 +238,53 @@ fn gnerate_payload(args: &mut Args) -> Result<PayloadBuffer> {
|
||||||
let image = Image::new(&image_raw, Point::zero());
|
let image = Image::new(&image_raw, Point::zero());
|
||||||
payload.add_message_drawable(style, &image);
|
payload.add_message_drawable(style, &image);
|
||||||
}
|
}
|
||||||
|
Content::ImageFile { img_file } => {
|
||||||
|
let img_reader = ImageReader::open(img_file)?;
|
||||||
|
let img = img_reader
|
||||||
|
.decode()?
|
||||||
|
.resize(u32::MAX, 11, FilterType::Nearest)
|
||||||
|
.into_luma8();
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
let mut buffer = payload.add_message(style, (width as usize + 7) / 8);
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
if img.get_pixel(x, y).0 > [31] {
|
||||||
|
Pixel(Point::new(x.try_into()?, y.try_into()?), BinaryColor::On)
|
||||||
|
.draw(&mut buffer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Content::GifFile { gif_file } => {
|
||||||
|
let file_in = BufReader::new(File::open(gif_file)?);
|
||||||
|
let frames = GifDecoder::new(file_in)?
|
||||||
|
.into_frames()
|
||||||
|
.collect_frames()
|
||||||
|
.expect("error decoding gif");
|
||||||
|
|
||||||
|
let frame_count = frames.len();
|
||||||
|
let (width, height) = frames.first().unwrap().buffer().dimensions();
|
||||||
|
if height != 11 || width != 44 {
|
||||||
|
anyhow::bail!("Expected 44x11 pixel gif file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = payload.add_message(style, (48 * frame_count + 7) / 8);
|
||||||
|
|
||||||
|
for (i, frame) in frames.iter().enumerate() {
|
||||||
|
let buf = frame.buffer();
|
||||||
|
for y in 0..11 {
|
||||||
|
for x in 0..44 {
|
||||||
|
if buf.get_pixel(x, y).to_luma().0 > [31] {
|
||||||
|
Pixel(
|
||||||
|
Point::new((x as usize + i * 48).try_into()?, y.try_into()?),
|
||||||
|
BinaryColor::On,
|
||||||
|
)
|
||||||
|
.draw(&mut buffer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
//! Protocol used to update the badge
|
//! Protocol used to update the badge
|
||||||
|
|
||||||
use std::num::TryFromIntError;
|
|
||||||
|
|
||||||
#[cfg(feature = "embedded-graphics")]
|
#[cfg(feature = "embedded-graphics")]
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
draw_target::DrawTarget,
|
draw_target::DrawTarget,
|
||||||
|
@ -11,6 +9,7 @@ use embedded_graphics::{
|
||||||
primitives::Rectangle,
|
primitives::Rectangle,
|
||||||
Drawable,
|
Drawable,
|
||||||
};
|
};
|
||||||
|
use std::num::TryFromIntError;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, KnownLayout, U16};
|
use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, KnownLayout, U16};
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ impl Style {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a dotted border arround the display.
|
/// Show a dotted border around the display.
|
||||||
/// ```
|
/// ```
|
||||||
/// use badgemagic::protocol::Style;
|
/// use badgemagic::protocol::Style;
|
||||||
/// # (
|
/// # (
|
||||||
|
@ -161,7 +160,7 @@ impl TryFrom<u8> for Speed {
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
/// Scroll thorugh the message from left to right
|
/// Scroll through the message from left to right
|
||||||
#[default]
|
#[default]
|
||||||
Left,
|
Left,
|
||||||
|
|
||||||
|
@ -193,14 +192,39 @@ pub enum Mode {
|
||||||
Laser,
|
Laser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display Brightness
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
|
||||||
|
pub enum Brightness {
|
||||||
|
#[default]
|
||||||
|
Full,
|
||||||
|
ThreeQuarters,
|
||||||
|
Half,
|
||||||
|
OneQuarter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Brightness> for u8 {
|
||||||
|
fn from(value: Brightness) -> Self {
|
||||||
|
match value {
|
||||||
|
Brightness::Full => 0x00,
|
||||||
|
Brightness::ThreeQuarters => 0x10,
|
||||||
|
Brightness::Half => 0x20,
|
||||||
|
Brightness::OneQuarter => 0x30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MSG_PADDING_ALIGN: usize = 64;
|
const MSG_PADDING_ALIGN: usize = 64;
|
||||||
|
|
||||||
const MAGIC: [u8; 6] = *b"wang\0\0";
|
const MAGIC: [u8; 5] = *b"wang\0";
|
||||||
|
|
||||||
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
|
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct Header {
|
struct Header {
|
||||||
magic: [u8; 6],
|
magic: [u8; 5],
|
||||||
|
brightness: u8,
|
||||||
blink: u8,
|
blink: u8,
|
||||||
border: u8,
|
border: u8,
|
||||||
speed_and_mode: [u8; 8],
|
speed_and_mode: [u8; 8],
|
||||||
|
@ -241,7 +265,7 @@ impl Timestamp {
|
||||||
|
|
||||||
/// Buffer to create a payload
|
/// Buffer to create a payload
|
||||||
///
|
///
|
||||||
/// A payload consits of up to 8 messages
|
/// A payload consists of up to 8 messages
|
||||||
/// ```
|
/// ```
|
||||||
/// # #[cfg(feature = "embedded-graphics")]
|
/// # #[cfg(feature = "embedded-graphics")]
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
|
@ -252,7 +276,7 @@ impl Timestamp {
|
||||||
/// primitives::{PrimitiveStyle, Rectangle, Styled},
|
/// primitives::{PrimitiveStyle, Rectangle, Styled},
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// let mut buffer = PayloadBuffer::new();
|
/// let mut buffer = PayloadBuffer::default();
|
||||||
/// buffer.add_message_drawable(
|
/// buffer.add_message_drawable(
|
||||||
/// Style::default(),
|
/// Style::default(),
|
||||||
/// &Styled::new(
|
/// &Styled::new(
|
||||||
|
@ -271,18 +295,19 @@ pub struct PayloadBuffer {
|
||||||
|
|
||||||
impl Default for PayloadBuffer {
|
impl Default for PayloadBuffer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new(Brightness::Full)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadBuffer {
|
impl PayloadBuffer {
|
||||||
/// Create a new empty buffer
|
/// Create a new empty buffer
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new(brightness: Brightness) -> Self {
|
||||||
Self {
|
Self {
|
||||||
num_messages: 0,
|
num_messages: 0,
|
||||||
data: Header {
|
data: Header {
|
||||||
magic: MAGIC,
|
magic: MAGIC,
|
||||||
|
brightness: brightness.into(),
|
||||||
blink: 0,
|
blink: 0,
|
||||||
border: 0,
|
border: 0,
|
||||||
speed_and_mode: [0; 8],
|
speed_and_mode: [0; 8],
|
||||||
|
@ -368,7 +393,7 @@ impl PayloadBuffer {
|
||||||
&self.data
|
&self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the payload buffe into bytes (with padding)
|
/// Convert the payload buffer into bytes (with padding)
|
||||||
#[allow(clippy::missing_panics_doc)] // should never panic
|
#[allow(clippy::missing_panics_doc)] // should never panic
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_padded_bytes(self) -> impl AsRef<[u8]> {
|
pub fn into_padded_bytes(self) -> impl AsRef<[u8]> {
|
||||||
|
@ -484,10 +509,9 @@ impl DrawTarget for MessageBuffer<'_> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use super::{Brightness, Speed};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use super::Speed;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn speed_to_u8_and_back() {
|
fn speed_to_u8_and_back() {
|
||||||
const VALID_SPEED_VALUES: Range<u8> = 1..8;
|
const VALID_SPEED_VALUES: Range<u8> = 1..8;
|
||||||
|
@ -499,4 +523,18 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brightness_to_u8() {
|
||||||
|
const VALID_BRIGHTNESS_VALUES: [(Brightness, u8); 4] = [
|
||||||
|
(Brightness::Full, 0x00),
|
||||||
|
(Brightness::ThreeQuarters, 0x10),
|
||||||
|
(Brightness::Half, 0x20),
|
||||||
|
(Brightness::OneQuarter, 0x30),
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in VALID_BRIGHTNESS_VALUES {
|
||||||
|
assert_eq!(u8::from(Brightness::from(i.0)), i.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub struct Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device {
|
impl Device {
|
||||||
/// Return a list of all usb devies as a string representation
|
/// Return a list of all usb devices as a string representation
|
||||||
pub fn list_all() -> Result<Vec<String>> {
|
pub fn list_all() -> Result<Vec<String>> {
|
||||||
let api = HidApi::new().context("create hid api")?;
|
let api = HidApi::new().context("create hid api")?;
|
||||||
let devices = api.device_list();
|
let devices = api.device_list();
|
||||||
|
@ -92,7 +92,7 @@ impl Device {
|
||||||
fn write_raw(device: &HidDevice, data: &[u8]) -> Result<()> {
|
fn write_raw(device: &HidDevice, data: &[u8]) -> Result<()> {
|
||||||
anyhow::ensure!(data.len() % 64 == 0, "payload not padded to 64 bytes");
|
anyhow::ensure!(data.len() % 64 == 0, "payload not padded to 64 bytes");
|
||||||
|
|
||||||
// the device will brick itself if the payload is too long (more then 8192 bytes)
|
// the device will brick itself if the payload is too long (more than 8192 bytes)
|
||||||
anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)");
|
anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)");
|
||||||
|
|
||||||
// just to be sure
|
// just to be sure
|
||||||
|
|
Loading…
Reference in a new issue