From 0f4b9384448253b1afff19596680f53a20d61772 Mon Sep 17 00:00:00 2001 From: Wouter R Date: Fri, 26 Jul 2013 10:18:55 +0200 Subject: [PATCH] preliminary printing support (through ultifi). --- src/conf_defaults.lua | 3 +- src/rest/api/api_printer.lua | 233 +++++++++++++++++++++++++++++++++++ src/rest/response.lua | 13 +- src/test/test_utils.lua | 19 +++ src/util/utils.lua | 7 ++ 5 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/rest/api/api_printer.lua diff --git a/src/conf_defaults.lua b/src/conf_defaults.lua index 13d0efb..7dec701 100644 --- a/src/conf_defaults.lua +++ b/src/conf_defaults.lua @@ -16,7 +16,7 @@ local M = {} --NOTE: pcall protects from invocation exceptions, which is what we need except --during debugging. This flag replaces them with a normal call so we can inspect stack traces. -M.DEBUG_PCALLS = true +M.DEBUG_PCALLS = false --This enables debugging of the REST API from the command-line, specify the path and optionally the request method as follows: 'p=/mod/func rq=POST' M.DEBUG_API = true @@ -24,6 +24,7 @@ M.DEBUG_API = true --REST responses will contain 'module' and 'function' keys describing what was requested M.API_INCLUDE_ENDPOINT_INFO = false +M.API_BASE_URL_PATH = 'doodle3d.com' -- includes any base path if necessary (e.g. 'localhost/~user') M.network_ap_ssid = { default = 'd3d-ap-%%MAC_ADDR_TAIL%%', diff --git a/src/rest/api/api_printer.lua b/src/rest/api/api_printer.lua new file mode 100644 index 0000000..68e42a4 --- /dev/null +++ b/src/rest/api/api_printer.lua @@ -0,0 +1,233 @@ +local log = require('util.logger') +local utils = require('util.utils') +local settings = require('util.settings') + +local M = { + isApi = true +} + + +local ULTIFI_BASE_PATH = '/tmp/UltiFi' +local TEMPERATURE_FILE = 'temp.out' +local COMMAND_FILE = 'command.in' +local GCODE_TMP_FILE = '/tmp/UltiFi/combined.gc' + +-- returns full path + ultifi path or nil +local function printerExists(id) + if id == nil then return nil end + + local path = '/dev/ttyACM' .. id + local upath = ULTIFI_BASE_PATH .. '/ttyACM' .. id + if utils.exists(path) then return path,upath end + + path = '/dev/ttyUSB' .. id + upath = ULTIFI_BASE_PATH .. '/ttyUSB' .. id + if utils.exists(path) then return path,upath end + + return nil +end + +-- returns printerId,devicePath,ultifiPath or nil if printer does not exist +-- when nil is returned, response has already been set as an error +local function getPrinterDataOrFail(request, response) + local id = tonumber(request:get("id")) + + if id == nil then + response:setError("missing id argument") + return nil + end + + local devpath,ultipath = printerExists(id) + + if not devpath then + response:setError("printer does not exist") + response:addData('id', id) + return nil + end + + return id,devpath,ultipath +end + +-- assumes printerPath exists, returns true if successful, false if command file already exists and is non-empty (i.e. printer busy), +-- nil+err if file could not be opened +local function sendGcode(printerPath, gcode) + local cmdPath = printerPath .. '/' .. COMMAND_FILE + local f,msg = io.open(cmdPath, 'a+') -- 'a+' is important, do not overwrite current contents in any case + + if not f then return nil,msg end + if utils.fileSize(f) > 0 then return false end + + log:debug("sending " .. gcode:len() .. " bytes of gcode") + f:write(gcode) + f:close() + + return true +end + +local function addToGcodeFile(gcode) + if not gcode or type(gcode) ~= 'string' then return nil,"missing gcode data" end + + local gcf,msg = io.open(GCODE_TMP_FILE, 'a+') + if not gcf then return nil,msg end + + log:debug("appending " .. gcode:len() .. " bytes of gcode to " .. GCODE_TMP_FILE) + gcf:write(gcode) + gcf:close() +end + +-- assumes printerPath exists, returns true if successful, false if command file already exists and is non-empty (i.e. printer busy), +-- nil+err if file could not be opened +local function printGcodeFile(printerPath) + local cmdPath = printerPath .. '/' .. COMMAND_FILE + local cmdf,msg = io.open(cmdPath, 'a+') -- 'a+' is important, do not overwrite current contents in any case + + if not cmdf then return nil,msg end + if utils.fileSize(cmdf) > 0 then return false end + + log:debug("starting print of gcode in " .. GCODE_TMP_FILE) + cmdf:write('(SENDFILE=' .. GCODE_TMP_FILE) + cmdf:close() + + return true +end + +local function isBusy(printerPath) + local cmdPath = printerPath .. '/' .. COMMAND_FILE + + if not utils.exists(cmdPath) then return false end + + local f,msg = io.open(cmdPath, 'r') + + if not f then return nil,msg end + local size = utils.fileSize(f) + f:close() + + return f > 0 +end + + +function M._global(request, response) + -- TODO: list all printers (based on /dev/ttyACM* and /dev/ttyUSB*) + response:setSuccess() +end + +--requires id(int) +--accepts with_raw(bool) to include raw printer response +function M.temperature(request, response) + local withRaw = utils.toboolean(request:get("with_raw")) + + local argId,devpath,ultipath = getPrinterDataOrFail(request, response) + if argId == nil then return end + + local f = io.open(ultipath .. '/' .. TEMPERATURE_FILE) + + if not f then + response:setError("could not open temperature file") + response:addData('id', argId) + return + end + + local tempText = f:read('*all') + f:close() + + local hotend, hotendTarget, bed, bedTarget = tempText:match('T:(.*)%s+/(.*)%s+B:(.*)%s/(.*)%s+@.*') + + response:setSuccess() + if withRaw then response:addData('raw', tempText) end + response:addData('hotend', hotend) + response:addData('bed', bed) + response:addData('hotend_target', hotendTarget) + response:addData('bed_target', bedTarget) +end + +--requires id(int) +function M.busy(request, response) + local argId,devpath,ultipath = getPrinterDataOrFail(request, response) + if argId == nil then return end + + local b,msg = isBusy(ultipath) + + if b == nil then + response:setError("could not determine printer state") + response:addData('msg', msg) + else + response:setSuccess() + response:addData('busy', b) + end +end + + +function M.printing(request, response) + response:setError("not implemented") + response:addData('api_refer', response:apiURL('printer', 'busy')) +end + +--requires id(int) +function M.heatup_POST(request, response) + local argId,devpath,ultipath = getPrinterDataOrFail(request, response) + if argId == nil then return end + + local gcode = settings.get('printer.autoWarmUpCommand') + local rv,msg = sendGcode(ultipath, gcode) + + if rv then + response:setSuccess() + elseif rv == false then + response:setFail("printer is busy") + else + response:setError("could not send gcode") + response:addData('msg', msg) + end +end + +--requires id(int), gcode(string) +--accepts: first(bool) (chunks will be concatenated but output file will be cleared first if this argument is true) +--accepts: last(bool) (chunks will be concatenated and only when this argument is true will printing be started) +function M.print_POST(request, response) + local argId,devpath,ultipath = getPrinterDataOrFail(request, response) + if argId == nil then return end + + local argGcode = request:get("gcode") + local argIsFirst = utils.toboolean(request:get("first")) + local argIsLast = utils.toboolean(request:get("last")) + + if argGcode == nil or argGcode == '' then + response:setError("missing gcode argument") + return + end + + if argIsFirst == true then + log:debug("clearing all gcode in " .. GCODE_TMP_FILE) + response:addData('gcode_clear',true) + os.remove(GCODE_TMP_FILE) + end + + local rv,msg + + rv,msg = addToGcodeFile(argGcode) + if rv == nil then + response:setError("could not add gcode") + response:addData('msg', msg) + return + else + response:addData('gcode_append',gcode:len()) + end + + if argIsLast == true then + rv,msg = printGcodeFile(ultipath) + + if rv then + response:setSuccess() + response:addData('gcode_print',true) + elseif rv == false then + response:setFail("printer is busy") + else + response:setError("could not send gcode") + response:addData('msg', msg) + end + else + response:setSuccess() + end +end + +return M diff --git a/src/rest/response.lua b/src/rest/response.lua index 5ca887a..938ac28 100644 --- a/src/rest/response.lua +++ b/src/rest/response.lua @@ -1,5 +1,6 @@ local JSON = require('util/JSON') -local s = require('util.settings') +local settings = require('util.settings') +local defaults = require('conf_defaults') local M = {} M.__index = M @@ -28,7 +29,7 @@ function M.new(requestObject) local rqId = requestObject:get(REQUEST_ID_ARGUMENT) if rqId ~= nil then self.body[REQUEST_ID_ARGUMENT] = rqId end - if s.API_INCLUDE_ENDPOINT_INFO == true then + if settings.API_INCLUDE_ENDPOINT_INFO == true then self.body['module'] = requestObject:getRequestedApiModule() self.body['function'] = requestObject:getRealApiFunctionName() or '' end @@ -60,7 +61,7 @@ function M:setError(msg) self.body.status = 'error' if msg ~= '' then self.body.msg = msg end - self:addData('more_info', 'http://doodle3d.nl/wiki/wiki/communication-api') + self:addData('more_info', 'http://' .. defaults.API_BASE_URL_PATH .. '/wiki/wiki/communication-api') end --NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key @@ -69,6 +70,12 @@ function M:addData(k, v) self.body.data[k] = v end +function M:apiURL(mod, func) + if not mod then return nil end + if func then func = '/' .. func else func = "" end + return 'http://' .. defaults.API_BASE_URL_PATH .. '/cgi-bin/d3dapi/' .. mod .. func +end + function M:serializeAsJson() return JSON:encode(self.body) end diff --git a/src/test/test_utils.lua b/src/test/test_utils.lua index ada6205..1d919e9 100644 --- a/src/test/test_utils.lua +++ b/src/test/test_utils.lua @@ -94,6 +94,25 @@ function M:test_create() assert(actualContents == testContents) end +function M:test_size() + local tempFile = '/tmp/132uytjhgfr24e' + local text = 'Why is a raven like a writing-desk?' + + local f = io.open(tempFile, 'w') + f:write(text) + f:close() + + local f = io.open(tempFile, 'r') + assert(f:seek() == 0) + assert(utils.fileSize(f) == text:len()) + assert(f:seek() == 0) + f:read(4) + assert(utils.fileSize(f) == text:len()) + assert(f:seek() == 4) + f:close() + os.remove(tempFile) +end + function M:test_symlink() assert(false, 'not implemented') end diff --git a/src/util/utils.lua b/src/util/utils.lua index f8791df..40d22a8 100644 --- a/src/util/utils.lua +++ b/src/util/utils.lua @@ -12,6 +12,13 @@ function string:split(div) return arr end +function M.fileSize(file) + local current = file:seek() + local size = file:seek('end') + file:seek('set', current) + return size +end + function M.toboolean(s) if not s then return false end