use std::io::{Read, Write};
use byteorder::{ReadBytesExt, LittleEndian, WriteBytesExt};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Header {
pub print_time: u32,
pub filament_0_usage: u32,
pub filament_1_usage: u32,
pub multi_extruder_type: u16,
pub layer_height: u16,
pub reserved0: u16,
pub perimeter_shells: u16,
pub print_speed: u16,
pub hotbed_temp: u16,
pub extruder_0_temp: u16,
pub extruder_1_temp: u16,
pub reserved1: u16,
}
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct XGCode {
pub header: Header,
pub thumbnail: Vec<u8>,
pub gcode: Vec<u8>
}
#[derive(Copy,Clone,Debug,Eq,PartialEq)]
pub struct XGCodeRef<'a> {
pub header: Header,
pub thumbnail: &'a [u8],
pub gcode: &'a [u8],
}
#[derive(Debug,Error)]
pub enum Error {
#[error("Bad magic header")] BadMagic(Box<[u8; 16]>),
#[error("Bad header size")] BadHeaderSize(u32),
#[error("Thumb size negative")] ThumbSizeNegative(i32),
#[error("GCode too big")] ThumbnailTooLarge(usize),
#[error("Second goffset not found")] SecondGOffsetNotFound,
#[error("Data in reserved field")] DataInReservedField {offset: u16, value: u16},
#[error("IO error")] IO(#[from] std::io::Error),
}
const XGCODE_MAGIC: &'static [u8; 16] = b"xgcode 1.0\n\0\0\0\0\0";
const THUMB_OFFSET: u32 = 0x3A;
impl XGCode {
pub fn read<R: Read>(mut source: R) -> Result<Self, Error> {
let mut magic = [0; 16];
source.read_exact(&mut magic)?;
if &magic != XGCODE_MAGIC { return Err(Error::BadMagic(Box::new(magic))) }
let thumb_offset = source.read_u32::<LittleEndian>()?;
if thumb_offset != THUMB_OFFSET { return Err(Error::BadHeaderSize(thumb_offset)) }
let gcode_offset = source.read_u32::<LittleEndian>()?;
let thumb_size = (gcode_offset as usize).checked_sub(THUMB_OFFSET as usize)
.ok_or(Error::ThumbSizeNegative(gcode_offset as i32 - THUMB_OFFSET as i32))?;
let gcode_offset2 = source.read_u32::<LittleEndian>()?;
if gcode_offset != gcode_offset2 { return Err(Error::SecondGOffsetNotFound)}
let print_time = source.read_u32::<LittleEndian>()?;
let filament_0_usage = source.read_u32::<LittleEndian>()?;
let filament_1_usage = source.read_u32::<LittleEndian>()?;
let multi_extruder_type = source.read_u16::<LittleEndian>()?;
let layer_height = source.read_u16::<LittleEndian>()?;
let reserved0 = source.read_u16::<LittleEndian>()?;
let perimeter_shells = source.read_u16::<LittleEndian>()?;
let print_speed = source.read_u16::<LittleEndian>()?;
let hotbed_temp = source.read_u16::<LittleEndian>()?;
let extruder_0_temp = source.read_u16::<LittleEndian>()?;
let extruder_1_temp = source.read_u16::<LittleEndian>()?;
let reserved1 = source.read_u16::<LittleEndian>()?;
let header = Header { print_time, filament_0_usage, filament_1_usage, multi_extruder_type, layer_height, perimeter_shells, print_speed, hotbed_temp, extruder_0_temp, extruder_1_temp, reserved0, reserved1 };
let mut thumbnail = vec![0; thumb_size];
source.read_exact(&mut thumbnail)?;
let mut gcode = vec![];
source.read_to_end(&mut gcode)?;
Ok(XGCode{ header, thumbnail, gcode })
}
pub fn as_ref(&self) -> XGCodeRef {
XGCodeRef { header: self.header, thumbnail: &self.thumbnail[..], gcode: &self.gcode[..] }
}
pub fn write<W: Write>(&self, writer: W) -> Result<(), Error> {
self.as_ref().write(writer)
}
}
impl<'a> XGCodeRef<'a> {
fn write<W: Write>(&self, mut writer: W) -> Result<(), Error> {
let gcode_offset = THUMB_OFFSET as usize + self.thumbnail.len();
if gcode_offset > (u32::MAX as usize) { return Err(Error::ThumbnailTooLarge(self.thumbnail.len()))}
writer.write_all(XGCODE_MAGIC)?;
writer.write_u32::<LittleEndian>(THUMB_OFFSET)?;
writer.write_u32::<LittleEndian>(gcode_offset as u32)?;
writer.write_u32::<LittleEndian>(gcode_offset as u32)?;
writer.write_u32::<LittleEndian>(self.header.print_time)?;
writer.write_u32::<LittleEndian>(self.header.filament_0_usage)?;
writer.write_u32::<LittleEndian>(self.header.filament_1_usage)?;
writer.write_u16::<LittleEndian>(self.header.multi_extruder_type)?;
writer.write_u16::<LittleEndian>(self.header.layer_height)?;
writer.write_u16::<LittleEndian>(self.header.reserved0)?;
writer.write_u16::<LittleEndian>(self.header.perimeter_shells)?;
writer.write_u16::<LittleEndian>(self.header.print_speed)?;
writer.write_u16::<LittleEndian>(self.header.hotbed_temp)?;
writer.write_u16::<LittleEndian>(self.header.extruder_0_temp)?;
writer.write_u16::<LittleEndian>(self.header.extruder_1_temp)?;
writer.write_u16::<LittleEndian>(self.header.reserved1)?;
writer.write_all(self.thumbnail)?;
writer.write_all(self.gcode)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::XGCode;
#[test]
fn test_sample_file() {
let file = include_bytes!("../test/20mm_Box.gx");
let parsed = XGCode::read(&mut &file[..]).unwrap();
assert_eq!(&parsed.thumbnail[..2], b"BM");
let mut file2 = vec![];
parsed.write(&mut file2).unwrap();
assert_eq!(file, &file2[..]);
}
}