feat: nearly complete

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2023-04-05 01:22:24 +02:00
parent 9249edf1c4
commit dd330b7da9
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
13 changed files with 4375 additions and 1129 deletions

View File

@ -1,2 +0,0 @@
files/*
node_modules/

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules
files/
files/
dist/

Binary file not shown.

View File

@ -26,16 +26,23 @@ RUN npm run build
##############
FROM node:latest as PROD_IMAGE
# ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.5.2/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 ./
# RUN tar -xvf PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 -C /opt
# ENV PATH /opt/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201/bin:$PATH
# Download & Install Slic3r
# RUN apt-get update \
# && apt-get install -y --no-install-recommends \
# slic3r \
# && rm -rf /var/lib/apt/lists/*
# ENV SLICER_PATH slic3r
# Install deps
# Download & install PrusaSlicer
ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.5.2/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 ./
RUN tar -xvf PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201.tar.bz2 -C /opt
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
prusa-slicer \
&& apt-get install -y --no-install-recommends \
prusa-slicer \
&& apt-get remove prusa-slicer -y \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
ENV PATH /opt/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201/bin:$PATH
ENV SLICER_PATH prusa-slicer
# run as non root user
USER node
@ -43,14 +50,15 @@ USER node
# go to work folder
WORKDIR /home/node
ADD --chown=node:node https://github.com/bambulab/BambuStudio/releases/download/v01.05.00.61/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage ./
ENV SLICER_PATH /home/node/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage
RUN chmod +x /home/node/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage
# download & Install Bambu Studio
# ADD --chown=node:node https://github.com/bambulab/BambuStudio/releases/download/v01.05.00.61/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage ./
# ENV SLICER_PATH /home/node/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage
# RUN chmod +x /home/node/Bambu_Studio_linux_fedora_v01.05.00.61_20230314200047.AppImage
# copy from build image
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
COPY --chown=node:node --from=BUILD_IMAGE /home/node/configs ./configs
COPY --chown=node:node --from=BUILD_IMAGE /home/node/src ./src
COPY --chown=node:node --from=BUILD_IMAGE /home/node/dist ./dist
COPY --chown=node:node --from=BUILD_IMAGE /home/node/package.json /home/node/.env* ./
# Expose port

18
README.md Normal file
View File

@ -0,0 +1,18 @@
example command: `http://127.0.0.1:3000/?algo=total_filament_used_g%20*%200.6%20%2B%20(estimated_printing_time_seconds%20%2F%2060%20%2F%2060%20*%2040)`
example decoded algo: `total_filament_used_g * 0.6 + (estimated_printing_time_seconds / 60 / 60 * 40)`, it will result in `price` being the response to the algorithm
the .stl file MUST be the only thing in the body of the request
Query parameters:
- `algo`: the pricing algorithm
- `config`: use another config than the default one
other queries:
- run `prusa-slicer --help-fff` in the terminal or look at the params in the response to get the full list
examples:
- `layer_height` = `0.05`
- `scale` = `200%`
- `fill_density` = `90%`

293
configs/dzeio-printer.ini Normal file
View File

@ -0,0 +1,293 @@
# generated by PrusaSlicer 2.5.2 on 2023-04-04 at 23:17:15 UTC
avoid_crossing_perimeters = 0
avoid_crossing_perimeters_max_detour = 0
bed_custom_model =
bed_custom_texture =
bed_shape = 0x0,270x0,270x270,0x270
bed_temperature = 60
before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0
between_objects_gcode =
bottom_fill_pattern = monotonic
bottom_solid_layers = 4
bottom_solid_min_thickness = 1.2
bridge_acceleration = 500
bridge_angle = 0
bridge_fan_speed = 100
bridge_flow_ratio = 0.8
bridge_speed = 80
brim_separation = 0
brim_type = outer_only
brim_width = 0
clip_multipart_objects = 1
color_change_gcode = M600
colorprint_heights =
compatible_printers_condition_cummulative = "printer_model=~/(X1|Genius).*/ and nozzle_diameter[0]==0.4";
complete_objects = 0
cooling = 1
cooling_tube_length = 5
cooling_tube_retraction = 91.5
default_acceleration = 500
default_filament_profile = "Generic PLA @Artillery"
default_print_profile = 0.20mm NORMAL @Artillery
deretract_speed = 0
disable_fan_first_layers = 2
dont_support_bridges = 0
draft_shield = disabled
duplicate_distance = 6
elefant_foot_compensation = 0
end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n"
end_gcode = G91 ;Relative positionning\nG1 E-1 F2700 ;Retract a bit\nG1 E-1 Z0.2 F2400 ;Retract and raise Z\nG1 X5 Y5 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positionning\nG1 X0 Y150 ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\nM84 X Y E ;Disable all steppers but Z
ensure_vertical_shell_thickness = 1
external_perimeter_acceleration = 0
external_perimeter_extrusion_width = 0.4
external_perimeter_speed = 40
external_perimeters_first = 0
extra_loading_move = -2
extra_perimeters = 0
extruder_clearance_height = 25
extruder_clearance_radius = 45
extruder_colour = #FFFF00
extruder_offset = 0x0
extrusion_axis = E
extrusion_multiplier = 1
extrusion_width = 0.4
fan_always_on = 1
fan_below_layer_time = 60
filament_colour = #29B2B2
filament_cooling_final_speed = 3.4
filament_cooling_initial_speed = 2.2
filament_cooling_moves = 4
filament_cost = 19.95
filament_density = 1.24
filament_deretract_speed = nil
filament_diameter = 1.75
filament_load_time = 0
filament_loading_speed = 28
filament_loading_speed_start = 3
filament_max_volumetric_speed = 0
filament_minimal_purge_on_wipe_tower = 15
filament_notes = ""
filament_ramming_parameters = "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6"
filament_retract_before_travel = nil
filament_retract_before_wipe = nil
filament_retract_layer_change = nil
filament_retract_length = nil
filament_retract_lift = nil
filament_retract_lift_above = nil
filament_retract_lift_below = nil
filament_retract_restart_extra = nil
filament_retract_speed = nil
filament_settings_id = "PLA Francofil"
filament_soluble = 0
filament_spool_weight = 1000
filament_toolchange_delay = 0
filament_type = PLA
filament_unload_time = 0
filament_unloading_speed = 90
filament_unloading_speed_start = 100
filament_vendor = (Unknown)
filament_wipe = nil
fill_angle = 45
fill_density = 20%
fill_pattern = triangles
first_layer_acceleration = 500
first_layer_acceleration_over_raft = 0
first_layer_bed_temperature = 60
first_layer_extrusion_width = 0.4
first_layer_height = 0.2
first_layer_speed = 20
first_layer_speed_over_raft = 30
first_layer_temperature = 215
full_fan_speed_layer = 2
fuzzy_skin = none
fuzzy_skin_point_dist = 0.8
fuzzy_skin_thickness = 0.3
gap_fill_enabled = 1
gap_fill_speed = 30
gcode_comments = 1
gcode_flavor = marlin
gcode_label_objects = 1
gcode_resolution = 0.0125
gcode_substitutions =
high_current_on_filament_swap = 0
host_type = octoprint
infill_acceleration = 500
infill_anchor = 600%
infill_anchor_max = 50
infill_every_layers = 1
infill_extruder = 1
infill_extrusion_width = 0.45
infill_first = 0
infill_only_where_needed = 0
infill_overlap = 25%
infill_speed = 60
inherits_cummulative = "0.16mm OPTIMAL @Artillery";;"Sidewinder X1 BL-TOUCH"
interface_shells = 0
ironing = 0
ironing_flowrate = 15%
ironing_spacing = 0.1
ironing_speed = 15
ironing_type = top
layer_gcode = ;AFTER_LAYER_CHANGE\n;[layer_z]
layer_height = 0.16
machine_limits_usage = time_estimate_only
machine_max_acceleration_e = 1500,5000
machine_max_acceleration_extruding = 2000,1250
machine_max_acceleration_retracting = 2000,1250
machine_max_acceleration_travel = 1500,1250
machine_max_acceleration_x = 500,960
machine_max_acceleration_y = 500,960
machine_max_acceleration_z = 500,1000
machine_max_feedrate_e = 120,120
machine_max_feedrate_x = 150,100
machine_max_feedrate_y = 150,100
machine_max_feedrate_z = 12,12
machine_max_jerk_e = 1.5,1.5
machine_max_jerk_x = 8,8
machine_max_jerk_y = 8,8
machine_max_jerk_z = 0.4,0.4
machine_min_extruding_rate = 0,0
machine_min_travel_rate = 0,0
max_fan_speed = 100
max_layer_height = 0.3
max_print_height = 400
max_print_speed = 150
max_volumetric_extrusion_rate_slope_negative = 0
max_volumetric_extrusion_rate_slope_positive = 0
max_volumetric_speed = 0
min_bead_width = 85%
min_fan_speed = 80
min_feature_size = 25%
min_layer_height = 0.15
min_print_speed = 15
min_skirt_length = 4
mmu_segmented_region_max_width = 0
notes =
nozzle_diameter = 0.4
only_retract_when_crossing_perimeters = 0
ooze_prevention = 0
output_filename_format = {input_filename_base}_{layer_height}mm_{filament_type[0]}_{printer_model}_{print_time}.gcode
overhangs = 1
parking_pos_retraction = 92
pause_print_gcode =
perimeter_acceleration = 500
perimeter_extruder = 1
perimeter_extrusion_width = 0
perimeter_generator = arachne
perimeter_speed = 40
perimeters = 4
physical_printer_settings_id =
post_process =
print_host =
print_settings_id = PLA 1.75mm
printer_model = X1
printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_Artillery\nPRINTER_MODEL_X1
printer_settings_id = Sidewinder X1 nono
printer_technology = FFF
printer_variant = 0.4
printer_vendor =
printhost_apikey =
printhost_cafile =
raft_contact_distance = 0.1
raft_expansion = 1.5
raft_first_layer_density = 90%
raft_first_layer_expansion = 3
raft_layers = 0
remaining_times = 0
resolution = 0
retract_before_travel = 1
retract_before_wipe = 80%
retract_layer_change = 0
retract_length = 0.8
retract_length_toolchange = 4
retract_lift = 0.2
retract_lift_above = 0
retract_lift_below = 380
retract_restart_extra = 0
retract_restart_extra_toolchange = 0
retract_speed = 30
seam_position = aligned
silent_mode = 0
single_extruder_multi_material = 0
single_extruder_multi_material_priming = 0
skirt_distance = 4
skirt_height = 1
skirts = 3
slice_closing_radius = 0.049
slicing_mode = regular
slowdown_below_layer_time = 2
small_perimeter_speed = 30
solid_infill_acceleration = 0
solid_infill_below_area = 0
solid_infill_every_layers = 0
solid_infill_extruder = 1
solid_infill_extrusion_width = 0.45
solid_infill_speed = 45
spiral_vase = 0
standby_temperature_delta = -5
start_filament_gcode = "; Filament gcode\n"
start_gcode = G28 ; home all axes\nG29;\nM420 S1; \nG92 E0 ;\nG1 Z3 F3000 ; move z up little to prevent scratching of surface\nG1 X20 Y5 Z0.3 F5000.0 ; move to start-line position\nG1 Z0.3 F1000 ; print height\nG1 X200 Y5 F1500.0 E15 ; draw 1st line\nG1 X200 Y5.3 Z0.3 F5000.0 ; move to side a little\nG1 X5.3 Y5.3 Z0.3 F1500.0 E30 ; draw 2nd line\nG1 Z3 F3000 ; move z up little to prevent scratching of surface\nG92 E0 ; Reset Extruder\n ; Reset Extruder\n
support_material = 1
support_material_angle = 0
support_material_auto = 1
support_material_bottom_contact_distance = 0
support_material_bottom_interface_layers = -1
support_material_buildplate_only = 1
support_material_closing_radius = 2
support_material_contact_distance = 0.3
support_material_enforce_layers = 0
support_material_extruder = 0
support_material_extrusion_width = 0.35
support_material_interface_contact_loops = 0
support_material_interface_extruder = 0
support_material_interface_layers = 2
support_material_interface_pattern = rectilinear
support_material_interface_spacing = 0.2
support_material_interface_speed = 100%
support_material_pattern = honeycomb
support_material_spacing = 2
support_material_speed = 60
support_material_style = snug
support_material_synchronize_layers = 0
support_material_threshold = 30
support_material_with_sheath = 1
support_material_xy_spacing = 50%
temperature = 210
template_custom_gcode =
thick_bridges = 0
thin_walls = 1
threads = 8
thumbnails =
thumbnails_format = PNG
toolchange_gcode =
top_fill_pattern = monotonic
top_infill_extrusion_width = 0.4
top_solid_infill_acceleration = 0
top_solid_infill_speed = 40
top_solid_layers = 4
top_solid_min_thickness = 0
travel_speed = 80
travel_speed_z = 0
use_firmware_retraction = 0
use_relative_e_distances = 1
use_volumetric_e = 0
variable_layer_height = 1
wall_distribution_count = 1
wall_transition_angle = 10
wall_transition_filter_deviation = 25%
wall_transition_length = 100%
wipe = 1
wipe_into_infill = 0
wipe_into_objects = 0
wipe_tower = 0
wipe_tower_bridging = 10
wipe_tower_brim_width = 2
wipe_tower_no_sparse_layers = 0
wipe_tower_rotation_angle = 0
wipe_tower_width = 60
wipe_tower_x = 170
wipe_tower_y = 125
wiping_volumes_extruders = 70,70
wiping_volumes_matrix = 0
xy_size_compensation = 0
z_offset = 0

BIN
file.stl

Binary file not shown.

4859
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,18 @@
{
"dependencies": {
"@dzeio/object-util": "^1.5.0",
"express": "^4.18.2"
"express": "^4.18.2",
"mathjs": "^11.8.0"
},
"scripts": {
"start": "node src/main.js",
"build": "true"
"dev": "ts-node-dev src/main.ts",
"start": "node dist/main.js",
"build": "tsc --project tsconfig.json"
},
"devDependencies": {
"@dzeio/config": "^1.1.12",
"@types/express": "^4.17.17",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.3"
}
}

64
src/gcodeUtils.ts Normal file
View File

@ -0,0 +1,64 @@
function parseNumber(str: string): number | undefined {
if (!/^(\d|\.)+$/g.test(str)) {
return undefined
}
const float = parseFloat(str)
const int = parseInt(str, 10)
if (isNaN(int)) {
return undefined
}
if (int !== float) {
return float
}
return int
}
function decodeTime(text: string): number {
let timeInSec = 0
for (const it of text.split(' ')) {
const lastChar = it.charAt(it.length - 1)
const time = parseInt(it.slice(0, it.length - 1), 10)
switch (lastChar) {
case 'm':
timeInSec += time * 60
break;
case 's':
timeInSec += time
break;
case 'h':
timeInSec += time * 60 * 60
break;
case 'd':
timeInSec += time * 60 * 60 * 24
break;
default:
throw new Error(`error parsing time ${it} (${time})`)
}
}
return timeInSec
}
export function getParams(data: string) {
const lines = data.split('\n').filter((it) => it.startsWith(';') && it.includes('='))
const obj: Record<string, string | number> = {}
for (const line of lines) {
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
let realKey = key.replace(/ /g, '_').replace(/\[|\]|\(|\)/g, '')
const realValue = parseNumber(value) ?? value
let offset = 0
while (obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] && obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] !== realValue) {
offset++
}
if (offset > 0) {
realKey = `${realKey}_${offset}`
}
if (obj[realKey] && obj[realKey] !== realValue) {
throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
}
obj[realKey] = parseNumber(value) ?? value
if (realKey === 'estimated_printing_time_normal_mode') {
obj['estimated_printing_time_seconds'] = decodeTime(value)
}
}
return obj
}

View File

@ -1,89 +0,0 @@
const express = require('express')
const fs = require('fs')
const execSync = require('child_process').execSync;
const { objectMap } = require('@dzeio/object-util')
const server = express()
fs.mkdirSync('files', { recursive: true })
/**
*
* @param {string} data the GCode source
* @returns {number} the time in seconds
*/
function decodeGCode(data) {
const line = data.split('\n').find((it) => it.includes('estimated printing time'))
if (!line) {
console.error('line not found :(')
process.exit(1)
}
const time = line.split('=')[1].trim()
let timeInSec = 0
for (const it of time.split(' ')) {
const lastChar = it.charAt(it.length - 1)
const time = parseInt(it.slice(0, it.length - 1), 10)
console.log(it)
switch (lastChar) {
case 'm':
timeInSec += time * 60
break;
case 's':
timeInSec += time
break;
case 'h':
timeInSec += time * 60 * 60
break;
case 'd':
timeInSec += time * 60 * 60 * 24
break;
default:
throw new Error(`error parsing time ${it} (${time})`)
}
}
console.log('orig', time, 'secs:', timeInSec, 'mins:', timeInSec / 60, 'hours:', timeInSec / 60 / 60, 'days:', timeInSec / 60 / 60 / 24)
return timeInSec
}
server.use ((req, res, next) => {
var data = '';
req.setEncoding('utf8');
req.on('data', function(chunk) {
data += chunk;
});
req.on('end', function() {
req.body = data;
next();
});
});
const path = process.cwd()
server.get('/', (req, res) => {
res.send('send through POST in body the stl')
})
const config = `${path}/configs/config.ini`
server.post('/', (req, res) => {
const overrides = req.query
const file = (Math.random() * 1000000).toFixed(0)
const stlPath = `${path}/files/${file}.stl`
const gcodePath = `${path}/files/${file}.gcode`
console.log(stlPath)
fs.writeFileSync(stlPath, req.body)
const additionnalParams = objectMap(overrides, (v, k) => `--${k} ${v}`).join(' ')
const cmd = `${process.env.SLICER_PATH} --export-gcode ${stlPath} --load ${config} --output ${gcodePath} ${additionnalParams}`
execSync(cmd)
const gcode = fs.readFileSync(gcodePath, 'utf-8')
const time = decodeGCode(gcode)
res.json({ timeToPrint: time, gcode })
// res.sendFile(gcodePath)
})
server.listen(3000, undefined, () => {
console.log(`🚀 Server ready at localhost:3000`);
})

123
src/main.ts Normal file
View File

@ -0,0 +1,123 @@
import express from 'express'
import fs from 'fs/promises'
import { exec as execAsync } from 'child_process'
import { objectMap, objectOmit } from '@dzeio/object-util'
import { getParams } from './gcodeUtils'
import util from 'util'
import { evaluate } from 'mathjs'
const exec = util.promisify(execAsync)
const server = express()
async function exists(file: string) {
try {
await fs.stat(file)
return true
} catch {
return false
}
}
// make the files dir
server.use((req, _, next) => {
let data: Array<any> = []
// req.setEncoding()
req.on('data', function(chunk) {
data.push(Buffer.from(chunk))
});
req.on('end', function() {
req.body = Buffer.concat(data);
next();
});
});
const path = process.cwd()
server.get('/', (_, res) => {
res.send('please read the README.md file...')
})
server.post('/', async (req, res) => {
const file = (Math.random() * 1000000).toFixed(0)
console.log('started processing new request', file)
await fs.mkdir('files', { recursive: true })
const overrides = objectOmit(req.query, 'algo', 'config')
let config = `${path}/configs/` + (req.query?.config ?? 'config') + '.ini'
if (!exists(config)) {
console.log('request finished in error :(', file)
res.status(403).send('Configuration file not existing')
return
}
const stlPath = `${path}/files/${file}.stl`
const gcodePath = `${path}/files/${file}.gcode`
// console.log(stlPath, req.body)
await fs.writeFile(stlPath, req.body, {
encoding: null
})
// console.log(fs.statSync(stlPath).size, req.body.length)
let additionnalParams = objectMap(overrides, (v, k) => `--${(k as string).replace(/_/g, '-')} ${v}`).join(' ')
const slicer = process.env.SLICER_PATH ?? 'prusa-slicer'
if (slicer.includes('prusa')) {
additionnalParams += ' --export-gcode'
}
const cmd = `${slicer} ${stlPath} --load ${config} --output ${gcodePath} ${additionnalParams}`
try {
await exec(cmd, {
timeout: 60000
})
} catch (e: any) {
console.log('request finished in error :(', file)
const line = e.toString()
console.error(e)
if (line.includes('Objects could not fit on the bed')) {
await fs.rm(stlPath)
res.status(403).send('Object is too large')
return
} else if (line.includes('No such file')) {
await fs.rm(stlPath)
res.status(403).send('Configuration file not existing')
return
} else if (line.includes('Unknown option')) {
await fs.rm(stlPath)
res.status(403).send('an override does not exists, please contact an administrator or refer to the documentation')
return
} else if (line.includes('ETIMEDOUT') || line.includes('Command failed')) {
await fs.rm(stlPath)
res.status(403).send('the file is taking too long to process :(')
return
}
res.status(500).send(`Error while making the file:<br />${e.toString().replace(cmd, '<i>software</i>').replace(stlPath, `<i>file ${file}</i>`).replace(`${path}/configs/`, '').replace('.ini', '')}`)
return
}
const gcode = await fs.readFile(gcodePath, 'utf-8')
await fs.rm(stlPath)
await fs.rm(gcodePath)
const params = getParams(gcode)
let price = -1
if (req.query?.algo) {
let algo = req.query.algo as string
// objectLoop(params, (value, key) => {
// if (typeof value !== 'number') {
// return
// }
// while (algo.includes(key)) {
// algo = algo.replace(key, value.toString())
// }
// })
price = evaluate(algo, params)
}
res.json({
price: parseFloat(price.toFixed(2)),
...getParams(gcode),
gcode
})
console.log('request successfull :)', file)
// res.sendFile(gcodePath)
})
server.listen(3000, () => {
console.log(`🚀 Server ready at localhost:3000`);
})

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@dzeio/config/tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"files": [
"src/main.ts"
]
}