Add first working implementation

This commit is contained in:
Jan-Henrik 2021-08-26 12:26:40 +02:00
commit 5341a01202
8 changed files with 1881 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1369
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
pub mod device;
pub mod image;
pub mod protocol;

129
src/protocol.rs Normal file
View 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
}
}