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