use serde_json::{Value, json};
#[derive(Debug, Default)]
pub struct SequenceIds {
next: u64,
}
impl SequenceIds {
pub fn new() -> Self {
Self::default()
}
pub fn next_id(&mut self) -> String {
let id = self.next;
self.next += 1;
id.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AmsControl {
Resume,
Reset,
Pause,
}
impl AmsControl {
pub fn as_str(self) -> &'static str {
match self {
AmsControl::Resume => "resume",
AmsControl::Reset => "reset",
AmsControl::Pause => "pause",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AmsFilamentSetting {
pub ams_id: u32,
pub tray_id: u32,
pub tray_info_idx: String,
pub tray_color: String,
pub nozzle_temp_min: i64,
pub nozzle_temp_max: i64,
pub tray_type: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LedNode {
ChamberLight,
WorkLight,
}
impl LedNode {
pub fn as_str(self) -> &'static str {
match self {
LedNode::ChamberLight => "chamber_light",
LedNode::WorkLight => "work_light",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimelapseControl {
Enable,
Disable,
}
impl TimelapseControl {
pub fn as_str(self) -> &'static str {
match self {
TimelapseControl::Enable => "enable",
TimelapseControl::Disable => "disable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpeedLevel {
Silent,
Standard,
Sport,
Ludicrous,
}
impl SpeedLevel {
pub fn level(self) -> i64 {
match self {
SpeedLevel::Silent => 1,
SpeedLevel::Standard => 2,
SpeedLevel::Sport => 3,
SpeedLevel::Ludicrous => 4,
}
}
pub fn from_level(n: i64) -> Option<Self> {
match n {
1 => Some(SpeedLevel::Silent),
2 => Some(SpeedLevel::Standard),
3 => Some(SpeedLevel::Sport),
4 => Some(SpeedLevel::Ludicrous),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
SpeedLevel::Silent => "silent",
SpeedLevel::Standard => "standard",
SpeedLevel::Sport => "sport",
SpeedLevel::Ludicrous => "ludicrous",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
PushAll,
GetVersion,
Pause,
Resume,
Stop,
CleanPrintError,
GcodeLine(String),
GcodeFile(String),
PrintSpeed(SpeedLevel),
ProjectFile(ProjectFile),
Led { node: LedNode, on: bool },
IpcamTimelapse(TimelapseControl),
Reboot,
AmsControl(AmsControl),
AmsChangeFilament {
target: u32,
curr_temp: i64,
tar_temp: i64,
},
AmsUserSetting {
ams_id: u32,
startup_read: bool,
tray_read: bool,
},
AmsFilamentSetting(Box<AmsFilamentSetting>),
Calibration {
bed_level: bool,
vibration: bool,
motor_noise: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectFile {
pub url: String,
pub plate: u32,
pub subtask_name: String,
pub md5: String,
pub bed_type: String,
pub use_ams: bool,
pub ams_mapping: Vec<i32>,
pub timelapse: bool,
pub flow_cali: bool,
pub bed_leveling: bool,
pub vibration_cali: bool,
pub layer_inspect: bool,
}
impl ProjectFile {
pub fn new(url: impl Into<String>, plate: u32, subtask_name: impl Into<String>) -> Self {
Self {
url: url.into(),
plate,
subtask_name: subtask_name.into(),
md5: String::new(),
bed_type: "auto".to_string(),
use_ams: false,
ams_mapping: Vec::new(),
timelapse: false,
flow_cali: true,
bed_leveling: true,
vibration_cali: true,
layer_inspect: true,
}
}
}
impl Command {
pub fn category(&self) -> &'static str {
match self {
Command::PushAll => "pushing",
Command::GetVersion => "info",
Command::Pause
| Command::Resume
| Command::Stop
| Command::CleanPrintError
| Command::GcodeLine(_)
| Command::GcodeFile(_)
| Command::PrintSpeed(_)
| Command::ProjectFile(_)
| Command::AmsControl(_)
| Command::AmsChangeFilament { .. }
| Command::AmsUserSetting { .. }
| Command::AmsFilamentSetting(_)
| Command::Calibration { .. } => "print",
Command::Led { .. } | Command::Reboot => "system",
Command::IpcamTimelapse(_) => "camera",
}
}
pub fn to_payload(&self, sequence_id: &str) -> Value {
match self {
Command::PushAll => json!({
"pushing": { "sequence_id": sequence_id, "command": "pushall" }
}),
Command::GetVersion => json!({
"info": { "sequence_id": sequence_id, "command": "get_version" }
}),
Command::Pause => print_command(sequence_id, "pause", ""),
Command::Resume => print_command(sequence_id, "resume", ""),
Command::Stop => print_command(sequence_id, "stop", ""),
Command::CleanPrintError => json!({
"print": {
"sequence_id": sequence_id,
"command": "clean_print_error",
"subtask_id": "0",
}
}),
Command::GcodeLine(line) => print_command(sequence_id, "gcode_line", line),
Command::GcodeFile(path) => print_command(sequence_id, "gcode_file", path),
Command::PrintSpeed(level) => {
print_command(sequence_id, "print_speed", &level.level().to_string())
}
Command::AmsControl(action) => json!({
"print": {
"sequence_id": sequence_id,
"command": "ams_control",
"param": action.as_str(),
}
}),
Command::AmsChangeFilament {
target,
curr_temp,
tar_temp,
} => json!({
"print": {
"sequence_id": sequence_id,
"command": "ams_change_filament",
"target": target,
"curr_temp": curr_temp,
"tar_temp": tar_temp,
}
}),
Command::AmsUserSetting {
ams_id,
startup_read,
tray_read,
} => json!({
"print": {
"sequence_id": sequence_id,
"command": "ams_user_setting",
"ams_id": ams_id,
"startup_read_option": startup_read,
"tray_read_option": tray_read,
}
}),
Command::AmsFilamentSetting(s) => json!({
"print": {
"sequence_id": sequence_id,
"command": "ams_filament_setting",
"ams_id": s.ams_id,
"tray_id": s.tray_id,
"tray_info_idx": s.tray_info_idx,
"tray_color": s.tray_color,
"nozzle_temp_min": s.nozzle_temp_min,
"nozzle_temp_max": s.nozzle_temp_max,
"tray_type": s.tray_type,
}
}),
Command::ProjectFile(p) => json!({
"print": {
"sequence_id": sequence_id,
"command": "project_file",
"param": format!("Metadata/plate_{}.gcode", p.plate),
"url": p.url,
"subtask_name": p.subtask_name,
"md5": p.md5,
"bed_type": p.bed_type,
"timelapse": p.timelapse,
"flow_cali": p.flow_cali,
"bed_leveling": p.bed_leveling,
"vibration_cali": p.vibration_cali,
"layer_inspect": p.layer_inspect,
"use_ams": p.use_ams,
"ams_mapping": p.ams_mapping,
"project_id": "0",
"profile_id": "0",
"task_id": "0",
"subtask_id": "0",
}
}),
Command::Calibration {
bed_level,
vibration,
motor_noise,
} => {
let option = i64::from(*bed_level) * 2
+ i64::from(*vibration) * 4
+ i64::from(*motor_noise) * 8;
json!({
"print": { "sequence_id": sequence_id, "command": "calibration", "option": option }
})
}
Command::Led { node, on } => json!({
"system": {
"sequence_id": sequence_id,
"command": "ledctrl",
"led_node": node.as_str(),
"led_mode": if *on { "on" } else { "off" },
"led_on_time": 500,
"led_off_time": 500,
"loop_times": 0,
"interval_time": 0,
}
}),
Command::Reboot => json!({
"system": { "sequence_id": sequence_id, "command": "reboot" }
}),
Command::IpcamTimelapse(control) => json!({
"camera": {
"sequence_id": sequence_id,
"command": "ipcam_timelapse",
"control": control.as_str(),
}
}),
}
}
}
fn print_command(sequence_id: &str, command: &str, param: &str) -> Value {
json!({
"print": { "sequence_id": sequence_id, "command": command, "param": param }
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sequence_ids_are_monotonic_strings_from_zero() {
let mut ids = SequenceIds::new();
assert_eq!(ids.next_id(), "0");
assert_eq!(ids.next_id(), "1");
assert_eq!(ids.next_id(), "2");
}
#[test]
fn categories_match_the_envelope_key() {
assert_eq!(Command::PushAll.category(), "pushing");
assert_eq!(Command::Pause.category(), "print");
assert_eq!(Command::GcodeLine("G28".into()).category(), "print");
assert_eq!(Command::GcodeFile("/x".into()).category(), "print");
assert_eq!(
Command::ProjectFile(ProjectFile::new("u", 1, "n")).category(),
"print"
);
assert_eq!(
Command::Led {
node: LedNode::ChamberLight,
on: true
}
.category(),
"system"
);
}
#[test]
fn calibration_option_is_a_bitmask() {
let v = Command::Calibration {
bed_level: true,
vibration: true,
motor_noise: false,
}
.to_payload("1");
assert_eq!(v["print"]["command"], "calibration");
assert_eq!(v["print"]["option"], 6); assert_eq!(
Command::Calibration {
bed_level: false,
vibration: false,
motor_noise: true,
}
.to_payload("1")["print"]["option"],
8
);
}
#[test]
fn clean_print_error_payload() {
let v = Command::CleanPrintError.to_payload("3");
assert_eq!(v["print"]["command"], "clean_print_error");
assert_eq!(v["print"]["sequence_id"], "3");
assert_eq!(v["print"]["subtask_id"], "0");
assert_eq!(Command::CleanPrintError.category(), "print");
}
#[test]
fn gcode_file_payload() {
assert_eq!(
Command::GcodeFile("/cache/foo.gcode".into()).to_payload("2"),
json!({ "print": { "sequence_id": "2", "command": "gcode_file", "param": "/cache/foo.gcode" } })
);
}
#[test]
fn project_file_payload_has_plate_and_lan_ids() {
let pf = ProjectFile::new("ftp:///cache/x.gcode.3mf", 2, "x job");
let v = Command::ProjectFile(pf).to_payload("3");
let p = &v["print"];
assert_eq!(p["command"], "project_file");
assert_eq!(p["sequence_id"], "3");
assert_eq!(p["param"], "Metadata/plate_2.gcode");
assert_eq!(p["url"], "ftp:///cache/x.gcode.3mf");
assert_eq!(p["subtask_name"], "x job");
assert_eq!(p["use_ams"], false);
assert_eq!(p["task_id"], "0"); assert!(p["ams_mapping"].is_array());
}
#[test]
fn get_version_is_an_info_read() {
assert_eq!(Command::GetVersion.category(), "info");
assert_eq!(
Command::GetVersion.to_payload("1"),
json!({ "info": { "sequence_id": "1", "command": "get_version" } })
);
}
#[test]
fn pushall_payload() {
assert_eq!(
Command::PushAll.to_payload("0"),
json!({ "pushing": { "sequence_id": "0", "command": "pushall" } })
);
}
#[test]
fn pause_resume_stop_payloads() {
assert_eq!(
Command::Pause.to_payload("3"),
json!({ "print": { "sequence_id": "3", "command": "pause", "param": "" } })
);
assert_eq!(
Command::Resume.to_payload("4"),
json!({ "print": { "sequence_id": "4", "command": "resume", "param": "" } })
);
assert_eq!(
Command::Stop.to_payload("5"),
json!({ "print": { "sequence_id": "5", "command": "stop", "param": "" } })
);
}
#[test]
fn gcode_line_payload_carries_the_line_in_param() {
assert_eq!(
Command::GcodeLine("M104 S210".to_string()).to_payload("7"),
json!({ "print": { "sequence_id": "7", "command": "gcode_line", "param": "M104 S210" } })
);
}
#[test]
fn ledctrl_on_and_off_payloads_carry_the_node() {
let on = Command::Led {
node: LedNode::ChamberLight,
on: true,
}
.to_payload("8");
assert_eq!(on["system"]["command"], "ledctrl");
assert_eq!(on["system"]["led_node"], "chamber_light");
assert_eq!(on["system"]["led_mode"], "on");
assert_eq!(on["system"]["sequence_id"], "8");
let off = Command::Led {
node: LedNode::ChamberLight,
on: false,
}
.to_payload("9");
assert_eq!(off["system"]["led_mode"], "off");
let work = Command::Led {
node: LedNode::WorkLight,
on: true,
}
.to_payload("1");
assert_eq!(work["system"]["led_node"], "work_light");
}
#[test]
fn print_speed_renders_the_level_as_a_print_param() {
let v = Command::PrintSpeed(SpeedLevel::Sport).to_payload("6");
assert_eq!(
v,
json!({ "print": { "sequence_id": "6", "command": "print_speed", "param": "3" } })
);
assert_eq!(Command::PrintSpeed(SpeedLevel::Silent).category(), "print");
}
#[test]
fn speed_level_maps_to_and_from_its_number() {
for (lvl, n) in [
(SpeedLevel::Silent, 1),
(SpeedLevel::Standard, 2),
(SpeedLevel::Sport, 3),
(SpeedLevel::Ludicrous, 4),
] {
assert_eq!(lvl.level(), n);
assert_eq!(SpeedLevel::from_level(n), Some(lvl));
}
assert_eq!(SpeedLevel::from_level(0), None);
assert_eq!(SpeedLevel::from_level(5), None);
}
#[test]
fn ipcam_timelapse_is_a_camera_command() {
assert_eq!(
Command::IpcamTimelapse(TimelapseControl::Enable).category(),
"camera"
);
let on = Command::IpcamTimelapse(TimelapseControl::Enable).to_payload("4");
assert_eq!(on["camera"]["command"], "ipcam_timelapse");
assert_eq!(on["camera"]["control"], "enable");
assert_eq!(on["camera"]["sequence_id"], "4");
let off = Command::IpcamTimelapse(TimelapseControl::Disable).to_payload("5");
assert_eq!(off["camera"]["control"], "disable");
}
#[test]
fn ams_control_payload_matches_spec() {
let v = Command::AmsControl(AmsControl::Resume).to_payload("1");
assert_eq!(
v,
json!({ "print": { "sequence_id": "1", "command": "ams_control", "param": "resume" } })
);
assert_eq!(
Command::AmsControl(AmsControl::Reset).to_payload("1")["print"]["param"],
"reset"
);
assert_eq!(Command::AmsControl(AmsControl::Pause).category(), "print");
}
#[test]
fn ams_change_filament_payload_matches_spec() {
let v = Command::AmsChangeFilament {
target: 2,
curr_temp: 220,
tar_temp: 240,
}
.to_payload("1");
let p = &v["print"];
assert_eq!(p["command"], "ams_change_filament");
assert_eq!(p["target"], 2);
assert_eq!(p["curr_temp"], 220);
assert_eq!(p["tar_temp"], 240);
}
#[test]
fn ams_user_setting_payload_matches_spec() {
let v = Command::AmsUserSetting {
ams_id: 0,
startup_read: true,
tray_read: false,
}
.to_payload("1");
let p = &v["print"];
assert_eq!(p["command"], "ams_user_setting");
assert_eq!(p["ams_id"], 0);
assert_eq!(p["startup_read_option"], true);
assert_eq!(p["tray_read_option"], false);
}
#[test]
fn ams_filament_setting_payload_matches_spec() {
let v = Command::AmsFilamentSetting(Box::new(AmsFilamentSetting {
ams_id: 0,
tray_id: 1,
tray_info_idx: "GFA00".to_string(),
tray_color: "00112233".to_string(),
nozzle_temp_min: 190,
nozzle_temp_max: 230,
tray_type: "PLA".to_string(),
}))
.to_payload("1");
let p = &v["print"];
assert_eq!(p["command"], "ams_filament_setting");
assert_eq!(p["ams_id"], 0);
assert_eq!(p["tray_id"], 1);
assert_eq!(p["tray_info_idx"], "GFA00");
assert_eq!(p["tray_color"], "00112233");
assert_eq!(p["nozzle_temp_min"], 190);
assert_eq!(p["nozzle_temp_max"], 230);
assert_eq!(p["tray_type"], "PLA");
}
#[test]
fn reboot_is_a_system_command() {
assert_eq!(Command::Reboot.category(), "system");
assert_eq!(
Command::Reboot.to_payload("3"),
json!({ "system": { "sequence_id": "3", "command": "reboot" } })
);
}
#[test]
fn sequence_id_is_serialised_as_a_string_not_a_number() {
let payload = Command::PushAll.to_payload("42");
assert!(payload["pushing"]["sequence_id"].is_string());
}
#[test]
fn rendering_does_not_consume_or_mutate_the_command() {
let cmd = Command::GcodeLine("G28".to_string());
let _ = cmd.to_payload("0");
assert_eq!(cmd, Command::GcodeLine("G28".to_string()));
}
}