mirror of
https://github.com/jhbruhn/catprint-rs.git
synced 2025-03-14 18:35:49 +00:00
Add first working implementation
This commit is contained in:
commit
5341a01202
8 changed files with 1881 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1369
Cargo.lock
generated
Normal file
1369
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "catprint"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
btleplug = "0.8"
|
||||||
|
uuid = "0.8"
|
||||||
|
futures = "0.3"
|
||||||
|
tokio = {version = "1.10", features = ["rt", "macros", "rt-multi-thread"] }
|
||||||
|
crc = { git = "ssh://git@github.com/mrhooray/crc-rs.git" }
|
||||||
|
dither = "=1.3.7" # last version without nightly requirement
|
||||||
|
image = "0.23"
|
||||||
|
clap = "3.0.0-beta.4"
|
121
src/bin/catprint.rs
Normal file
121
src/bin/catprint.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use btleplug::platform::Manager;
|
||||||
|
use catprint::*;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use clap::{crate_authors, crate_version, AppSettings, ArgEnum, Clap};
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
#[clap(version = crate_version!(), author = crate_authors!())]
|
||||||
|
#[clap(setting = AppSettings::ColoredHelp)]
|
||||||
|
struct Opts {
|
||||||
|
/// Set the device name of your printer.
|
||||||
|
#[clap(short, long, default_value = "GB02")]
|
||||||
|
device_name: String,
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcmd: SubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
enum SubCommand {
|
||||||
|
/// Move the paper without printing
|
||||||
|
Feed(Feed),
|
||||||
|
|
||||||
|
/// Print an image
|
||||||
|
Print(Print),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
struct Feed {
|
||||||
|
/// Print debug info
|
||||||
|
#[clap(short, long)]
|
||||||
|
backward: bool,
|
||||||
|
/// amount to feed the paper
|
||||||
|
length: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
struct Print {
|
||||||
|
/// The image you want to print out
|
||||||
|
input_image: String,
|
||||||
|
|
||||||
|
/// The ditherer supposed to be used. none is good for text and vector graphics
|
||||||
|
#[clap(arg_enum, short, long, default_value = "k-mean")]
|
||||||
|
ditherer: Ditherers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ArgEnum)]
|
||||||
|
enum Ditherers {
|
||||||
|
None,
|
||||||
|
KMean,
|
||||||
|
Atkinson,
|
||||||
|
Burkes,
|
||||||
|
FloydSteinberg,
|
||||||
|
JarvisJudiceNinke,
|
||||||
|
Sierra3,
|
||||||
|
Stucki,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
|
||||||
|
let mut manager = Manager::new().await?;
|
||||||
|
|
||||||
|
let device = device::Device::find(&mut manager, &opts.device_name).await?;
|
||||||
|
|
||||||
|
match opts.subcmd {
|
||||||
|
SubCommand::Feed(feed) => main_feed(device, feed).await,
|
||||||
|
SubCommand::Print(print) => main_print(device, print).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn main_feed(mut device: device::Device, feed: Feed) -> Result<(), Box<dyn Error>> {
|
||||||
|
let feed_direction = if feed.backward {
|
||||||
|
protocol::FeedDirection::Reverse
|
||||||
|
} else {
|
||||||
|
protocol::FeedDirection::Forward
|
||||||
|
};
|
||||||
|
|
||||||
|
device.queue_command(protocol::Command::Feed(feed_direction, feed.length));
|
||||||
|
|
||||||
|
device.flush().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn main_print(mut device: device::Device, print: Print) -> Result<(), Box<dyn Error>> {
|
||||||
|
device.queue_command(protocol::Command::Feed(
|
||||||
|
protocol::FeedDirection::Forward,
|
||||||
|
10,
|
||||||
|
));
|
||||||
|
|
||||||
|
let image = image::Image::load(&std::path::PathBuf::from(print.input_image)).unwrap();
|
||||||
|
|
||||||
|
let image = match print.ditherer {
|
||||||
|
Ditherers::None => image,
|
||||||
|
Ditherers::KMean => image.kmean(),
|
||||||
|
Ditherers::Atkinson => image.dither(&dither::ditherer::ATKINSON),
|
||||||
|
Ditherers::Burkes => image.dither(&dither::ditherer::BURKES),
|
||||||
|
Ditherers::FloydSteinberg => image.dither(&dither::ditherer::FLOYD_STEINBERG),
|
||||||
|
Ditherers::JarvisJudiceNinke => image.dither(&dither::ditherer::JARVIS_JUDICE_NINKE),
|
||||||
|
Ditherers::Sierra3 => image.dither(&dither::ditherer::SIERRA_3),
|
||||||
|
Ditherers::Stucki => image.dither(&dither::ditherer::STUCKI),
|
||||||
|
};
|
||||||
|
|
||||||
|
let quality = protocol::Quality::Quality3;
|
||||||
|
let energy = 12000;
|
||||||
|
let mode = match print.ditherer {
|
||||||
|
Ditherers::None | Ditherers::KMean => protocol::DrawingMode::Text,
|
||||||
|
_ => protocol::DrawingMode::Image,
|
||||||
|
};
|
||||||
|
|
||||||
|
let print = image.print(mode, quality, energy);
|
||||||
|
|
||||||
|
device.queue_commands(&print);
|
||||||
|
device.queue_command(protocol::Command::Feed(
|
||||||
|
protocol::FeedDirection::Forward,
|
||||||
|
150,
|
||||||
|
));
|
||||||
|
|
||||||
|
device.flush().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
112
src/device.rs
Normal file
112
src/device.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use btleplug::api::{Central, Manager as _, Peripheral};
|
||||||
|
use btleplug::platform::Manager;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const TX_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x0000ae01_0000_1000_8000_00805f9b34fb);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Device {
|
||||||
|
peripheral: btleplug::platform::Peripheral,
|
||||||
|
tx_buffer: VecDeque<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
pub async fn find(manager: &mut Manager, name: &str) -> Result<Device, Box<dyn Error>> {
|
||||||
|
let mut device_result = Err("Could not connect to device".into());
|
||||||
|
let adapter_list = manager.adapters().await?;
|
||||||
|
if adapter_list.is_empty() {
|
||||||
|
device_result = Err("No Bluetooth adapters found".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
for adapter in adapter_list.iter() {
|
||||||
|
println!("Starting scan...");
|
||||||
|
adapter
|
||||||
|
.start_scan()
|
||||||
|
.await
|
||||||
|
.expect("Can't scan BLE adapter for connected devices...");
|
||||||
|
time::sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
let peripherals = adapter.peripherals().await?;
|
||||||
|
for peripheral in peripherals {
|
||||||
|
let properties = peripheral.properties().await?;
|
||||||
|
|
||||||
|
if let Some(properties) = properties {
|
||||||
|
if let Some(local_name) = properties.local_name {
|
||||||
|
if local_name.contains(name) {
|
||||||
|
if !peripheral.is_connected().await? {
|
||||||
|
peripheral.connect().await?;
|
||||||
|
}
|
||||||
|
let _ = peripheral.discover_characteristics().await?;
|
||||||
|
device_result = Ok(Device {
|
||||||
|
peripheral,
|
||||||
|
tx_buffer: VecDeque::new(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device_result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_command(&mut self, command: crate::protocol::Command) {
|
||||||
|
self.tx_buffer.extend(&command.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_commands(&mut self, commands: &[crate::protocol::Command]) {
|
||||||
|
for command in commands {
|
||||||
|
self.queue_command(*command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn flush(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
|
const MTU_SIZE: usize = 20;
|
||||||
|
|
||||||
|
while !self.tx_buffer.is_empty() {
|
||||||
|
let mut buf = Vec::with_capacity(MTU_SIZE);
|
||||||
|
for _ in 0..MTU_SIZE {
|
||||||
|
if let Some(byte) = self.tx_buffer.pop_front() {
|
||||||
|
buf.push(byte);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this could be nicer i guess
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tx(&buf).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tx(&mut self, data: &[u8]) -> Result<(), Box<dyn Error>> {
|
||||||
|
let characteristics = self.peripheral.characteristics();
|
||||||
|
|
||||||
|
let tx_characteristic = characteristics
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.uuid == TX_CHARACTERISTIC_UUID)
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.peripheral
|
||||||
|
.write(
|
||||||
|
&tx_characteristic,
|
||||||
|
data,
|
||||||
|
btleplug::api::WriteType::WithoutResponse,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn destroy(self) {
|
||||||
|
self.peripheral.disconnect().await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
130
src/image.rs
Normal file
130
src/image.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::protocol::*;
|
||||||
|
use dither::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct Image {
|
||||||
|
image: Img<f64>,
|
||||||
|
mean: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rle_bytes(val: u8, mut counter: u32) -> Vec<u8> {
|
||||||
|
let mut compressed = vec![];
|
||||||
|
if counter > 0 {
|
||||||
|
while counter > 127 {
|
||||||
|
let code = (val << 7) | 127;
|
||||||
|
compressed.push(code);
|
||||||
|
counter -= 127;
|
||||||
|
}
|
||||||
|
let code = (val << 7) | (counter as u8);
|
||||||
|
compressed.push(code);
|
||||||
|
}
|
||||||
|
compressed
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn load(path: &Path) -> std::result::Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let image = image::open(path)?;
|
||||||
|
|
||||||
|
let image = image
|
||||||
|
.resize(
|
||||||
|
crate::protocol::PIXELS_PER_LINE as u32,
|
||||||
|
!0_u32,
|
||||||
|
image::imageops::FilterType::CatmullRom,
|
||||||
|
)
|
||||||
|
.into_rgb8();
|
||||||
|
|
||||||
|
let image: Img<RGB<u8>> = unsafe {
|
||||||
|
Img::from_raw_buf(
|
||||||
|
image.pixels().map(|p| RGB::from(p.0)).collect(),
|
||||||
|
image.width(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let image: Img<RGB<f64>> = image.convert_with(|rgb| rgb.convert_with(f64::from));
|
||||||
|
|
||||||
|
let image = image.convert_with(|rgb| rgb.to_chroma_corrected_black_and_white());
|
||||||
|
Ok(Self { image, mean: 0.5 })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kmean(self) -> Self {
|
||||||
|
let mean = self.image.iter().sum::<f64>() / self.image.len() as f64;
|
||||||
|
Self {
|
||||||
|
image: self.image,
|
||||||
|
mean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dither(self, ditherer: &Ditherer) -> Self {
|
||||||
|
let image = ditherer.dither(self.image, dither::create_quantize_n_bits_func(1).unwrap());
|
||||||
|
Self { image, mean: 0.5 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a line, bool is true if compressed, false if uncompressed
|
||||||
|
pub fn line(
|
||||||
|
&self,
|
||||||
|
y: u32,
|
||||||
|
) -> Option<(bool, usize, [u8; crate::protocol::PIXELS_PER_LINE / 8])> {
|
||||||
|
if y > self.image.height() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let mut compressed = Vec::<u8>::new();
|
||||||
|
|
||||||
|
let mut counter = 0_u32;
|
||||||
|
let mut last_val = 2;
|
||||||
|
for x in 0..crate::protocol::PIXELS_PER_LINE {
|
||||||
|
let x = x as u32;
|
||||||
|
let pixel = self.image.get((x, y)).unwrap();
|
||||||
|
let val = if pixel > &self.mean { 0 } else { 1 };
|
||||||
|
if val == last_val {
|
||||||
|
counter = counter + 1
|
||||||
|
} else if counter > 0 {
|
||||||
|
compressed.extend(rle_bytes(last_val, counter));
|
||||||
|
counter = 1;
|
||||||
|
}
|
||||||
|
last_val = val;
|
||||||
|
}
|
||||||
|
compressed.extend(rle_bytes(last_val, counter));
|
||||||
|
|
||||||
|
if compressed.len() > crate::protocol::PIXELS_PER_LINE / 8 {
|
||||||
|
let mut data = [0_u8; crate::protocol::PIXELS_PER_LINE / 8];
|
||||||
|
for x in 0..crate::protocol::PIXELS_PER_LINE {
|
||||||
|
let x = x as u32;
|
||||||
|
let pixel = self.image.get((x, y)).unwrap();
|
||||||
|
let val = if pixel > &self.mean { 0 } else { 1 };
|
||||||
|
let i = (x / 8) as usize;
|
||||||
|
let j = x % 8;
|
||||||
|
let current = data[i];
|
||||||
|
data[i] = current | (val << j);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((false, crate::protocol::PIXELS_PER_LINE / 8, data))
|
||||||
|
} else {
|
||||||
|
use std::convert::TryInto;
|
||||||
|
let len = compressed.len();
|
||||||
|
compressed.resize(crate::protocol::PIXELS_PER_LINE / 8, 0);
|
||||||
|
Some((true, len, compressed.try_into().unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_count(&self) -> u32 {
|
||||||
|
self.image.height()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(&self, mode: DrawingMode, quality: Quality, energy: u16) -> Vec<Command> {
|
||||||
|
let mut commands = vec![
|
||||||
|
Command::SetQuality(quality),
|
||||||
|
Command::SetEnergy(energy),
|
||||||
|
Command::SetDrawingMode(mode),
|
||||||
|
];
|
||||||
|
|
||||||
|
commands.push(Command::MagicLattice(LatticeType::Start));
|
||||||
|
for y in 0..self.line_count() {
|
||||||
|
let (compressed, len, pixels) = self.line(y).unwrap();
|
||||||
|
commands.push(Command::Print(compressed, len, pixels));
|
||||||
|
}
|
||||||
|
commands.push(Command::MagicLattice(LatticeType::End));
|
||||||
|
|
||||||
|
commands
|
||||||
|
}
|
||||||
|
}
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod device;
|
||||||
|
pub mod image;
|
||||||
|
pub mod protocol;
|
129
src/protocol.rs
Normal file
129
src/protocol.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
pub const PIXELS_PER_LINE: usize = 384;
|
||||||
|
const CCITT: crc::Crc<u8> = crc::Crc::<u8>::new(&crc::CRC_8_SMBUS);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Command {
|
||||||
|
Feed(FeedDirection, u8),
|
||||||
|
Print(bool, usize, [u8; PIXELS_PER_LINE / 8]), // One bit per pixel in uncompressed mode
|
||||||
|
GetDeviceStatus,
|
||||||
|
SetQuality(Quality),
|
||||||
|
MagicLattice(LatticeType),
|
||||||
|
GetDeviceInfo,
|
||||||
|
UpdateDevice, //???
|
||||||
|
SetWifi, //????
|
||||||
|
FlowControl(Flow),
|
||||||
|
SetEnergy(u16),
|
||||||
|
DeviceId(u8), //????
|
||||||
|
SetSpeed(u8),
|
||||||
|
SetDrawingMode(DrawingMode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Flow {
|
||||||
|
Start = 0x00,
|
||||||
|
Stop = 0x10,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum LatticeType {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum FeedDirection {
|
||||||
|
Forward,
|
||||||
|
Reverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum DrawingMode {
|
||||||
|
Image = 0x0,
|
||||||
|
Text = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Quality {
|
||||||
|
Quality1 = 0x31,
|
||||||
|
Quality2 = 0x32,
|
||||||
|
Quality3 = 0x33,
|
||||||
|
Quality4 = 0x34,
|
||||||
|
Quality5 = 0x35,
|
||||||
|
SpeedThin = 0x22,
|
||||||
|
SpeedModeration = 0x23,
|
||||||
|
SpeedThick = 0x25,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
fn opcode(&self) -> u8 {
|
||||||
|
use Command::*;
|
||||||
|
match self {
|
||||||
|
Feed(FeedDirection::Reverse, _) => 0xA0,
|
||||||
|
Feed(FeedDirection::Forward, _) => 0xA1,
|
||||||
|
Print(false, _, _) => 0xA2, // uncompressed
|
||||||
|
GetDeviceStatus => 0xA3,
|
||||||
|
SetQuality(_) => 0xA4,
|
||||||
|
MagicLattice(_) => 0xA6,
|
||||||
|
GetDeviceInfo => 0xA8,
|
||||||
|
UpdateDevice => 0xA9,
|
||||||
|
SetWifi => 0xAA,
|
||||||
|
FlowControl(_) => 0xAE,
|
||||||
|
SetEnergy(_) => 0xAF,
|
||||||
|
DeviceId(_) => 0xBB,
|
||||||
|
SetSpeed(_) => 0xBD,
|
||||||
|
SetDrawingMode(_) => 0xBE,
|
||||||
|
Print(true, _, _) => 0xBf, // compressed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload(&self) -> Vec<u8> {
|
||||||
|
use Command::*;
|
||||||
|
match self {
|
||||||
|
Feed(_, len) => vec![*len, 0],
|
||||||
|
Print(_, len, data) => data[0..*len].to_vec(),
|
||||||
|
GetDeviceStatus => vec![0],
|
||||||
|
SetQuality(quality) => vec![*quality as u8],
|
||||||
|
MagicLattice(LatticeType::Start) => vec![
|
||||||
|
0xAA, 0x55, 0x17, 0x38, 0x44, 0x5F, 0x5F, 0x5F, 0x44, 0x38, 0x2C,
|
||||||
|
],
|
||||||
|
MagicLattice(LatticeType::End) => vec![
|
||||||
|
0xAA, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17,
|
||||||
|
],
|
||||||
|
GetDeviceInfo => vec![0],
|
||||||
|
UpdateDevice => vec![0], // ????
|
||||||
|
FlowControl(flow) => vec![*flow as u8],
|
||||||
|
SetEnergy(energy) => vec![(energy & 0xFF) as u8, ((energy >> 8) & 0xFF) as u8],
|
||||||
|
DeviceId(id) => vec![*id], // ?????
|
||||||
|
SetSpeed(speed) => vec![*speed],
|
||||||
|
SetDrawingMode(mode) => vec![*mode as u8],
|
||||||
|
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
let opcode = self.opcode();
|
||||||
|
let payload = self.payload();
|
||||||
|
|
||||||
|
let mut crc = CCITT.digest();
|
||||||
|
crc.update(&payload);
|
||||||
|
let crc = crc.finalize() as u8;
|
||||||
|
|
||||||
|
let payload_len = payload.len();
|
||||||
|
|
||||||
|
let mut bytes = vec![
|
||||||
|
0x51,
|
||||||
|
0x78,
|
||||||
|
opcode,
|
||||||
|
0x00, /* Sent by host */
|
||||||
|
payload_len as u8,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
bytes.extend(payload);
|
||||||
|
|
||||||
|
bytes.extend(vec![crc, 0xFF]);
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue