diff --git a/src/conf_defaults.lua b/src/conf_defaults.lua index cc155c2..13d0efb 100644 --- a/src/conf_defaults.lua +++ b/src/conf_defaults.lua @@ -1,7 +1,18 @@ -local M = {} +--[[ + This file contains all valid configuration keys, their default values and optional constraints. + The table names are used as configuration key names, where underscores ('_') may be used to denote semi-categories. + The settings interface replaces periods ('.') by underscores so for instance 'network.ap.address' will + be translated to 'network_ap_address'. Multi-word names should be notated as camelCase. + Valid fields for the tables are: + - default: the default value (used when the key is not set in UCI config) + - type: used for basic type checking, one of bool, int, float or string + - description: A descriptive text usable by API clients + - min, max, regex: optional constraints (min and max constrain value for numbers, or length for strings) + + NOTE that the all-caps definitions will be changed into configuration keys, or moved to a different location +]]-- ---NOTE: proposed notation for baseline configuration (containing defaults as well as type and constraint information) ---the table name is the configuration key; min, max and regex are all optional; type is one of: {bool, int, float, string} +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. @@ -14,7 +25,7 @@ M.DEBUG_API = true M.API_INCLUDE_ENDPOINT_INFO = false -M.apSsid = { +M.network_ap_ssid = { default = 'd3d-ap-%%MAC_ADDR_TAIL%%', type = 'string', description = 'Access Point mode SSID', @@ -22,26 +33,133 @@ M.apSsid = { max = 32 } -M.apAddress = { +M.network_ap_address = { default = '192.168.10.1', type = 'string', description = 'Access Point mode IP address', regex = '%d+\.%d+\.%d+\.%d+' } -M.apNetmask = { +M.network_ap_netmask = { default = '255.255.255.0', type = 'string', description = 'Access Point mode netmask', regex = '%d+\.%d+\.%d+\.%d+' } -M.temperature = { +M.printer_temperature = { default = 230, type = 'int', description = '3D printer temperature', - min = 0, - max = 350 + min = 0 +} + +M.printer_objectHeight = { + default = 20, + type = 'int', + description = 'Maximum height that will be printed', + min = 0 +} + +M.printer_layerHeight = { + default = 0.2, + type = 'float', + description = '', + min = 0.0 +} + +M.printer_wallThickness = { + default = 0.5, + type = 'float', + description = '', + min = 0.0 +} + +M.printer_speed = { + default = 70, + type = 'int', + description = '', + min = 0 +} + +M.printer_travelSpeed = { + default = 200, + type = 'int', + description = '', + min = 0 +} + +M.printer_filamentThickness = { + default = 2.89, + type = 'float', + description = '', + min = 0.0 +} + +M.printer_useSubLayers = { + default = true, + type = 'bool', + description = 'Continuously move platform while printing instead of once per layer' +} + +M.printer_firstLayerSlow = { + default = true, + type = 'float', + description = 'Print the first layer slowly to get a more stable start', +} + +M.printer_autoWarmUp = { + default = true, + type = 'float', + description = '', +} + +M.printer_simplify_iterations = { + default = 10, + type = 'int', + description = '', + min = 0 +} + +M.printer_simplify_minNumPoints = { + default = 15, + type = 'int', + description = '', + min = 0 +} + +M.printer_simplify_minDistance = { + default = 3, + type = 'int', + description = '', + min = 0 +} + +M.printer_retraction_speed = { + default = 50, + type = 'int', + description = '', + min = 0 +} + +M.printer_retraction_minDistance = { + default = 5, + type = 'int', + description = '', + min = 0 +} + +M.printer_retraction_amount = { + default = 3, + type = 'int', + description = '', + min = 0 +} + +M.printer_autoWarmUpCommand = { + default = 'M104 S230', + type = 'string', + description = '' } return M diff --git a/src/test/test_settings.lua b/src/test/test_settings.lua index 39d84e1..0576b29 100644 --- a/src/test/test_settings.lua +++ b/src/test/test_settings.lua @@ -6,7 +6,7 @@ local uciConfigFileBackup = '/etc/config/wifibox.orig' local M = { _is_test = true, - _skip = { }, + _skip = { 'constraints' }, --FIXME: enabling constraints 'breaks' other tests _wifibox_only = { 'get' } } @@ -22,7 +22,7 @@ end function M:test_get() - local realKey, fakeKey = 'apAddress', 'theAnswer' + local realKey, fakeKey = 'network_ap_address', 'theQuestion' assert(not s.exists(fakeKey)) local fakeValue = s.get(fakeKey) @@ -31,40 +31,80 @@ function M:test_get() assert(s.exists(realKey)) local realValue = s.get(realKey) assert(realValue ~= nil) - assert(realValue == defaults.apAddress.default) + assert(realValue == defaults.network_ap_address.default) end function M:test_set() - local key, intKey = 'apAddress', 'temperature' - local intValue, goodValue, badValue1, badValue2 = 340, '10.0.0.1', '10.00.1', '10.0.0d.1' + local key, intKey, floatKey, boolKey = 'network_ap_address', 'printer_temperature', 'printer_filamentThickness', 'printer_useSubLayers' + local intValue, floatValue, boolValue = 340, 4.2, false + local value = '10.0.0.1' - assert(s.get(key) == defaults.apAddress.default) + assert(s.get(key) == defaults.network_ap_address.default) assert(s.isDefault(key)) - assert(s.set(key, goodValue)) - assert(s.get(key) == goodValue) + assert(s.set(key, value)) + assert(s.get(key) == value) assert(not s.isDefault(key)) + assert(s.set(key, nil)) + assert(s.isDefault(key)) + + -- test with value of int type + assert(s.get(intKey) == defaults.printer_temperature.default) + assert(s.isDefault(intKey)) + + assert(s.set(intKey, intValue)) + assert(s.get(intKey) == intValue) + assert(not s.isDefault(intKey)) + + -- test with value of float type + assert(s.get(floatKey) == defaults.printer_filamentThickness.default) + assert(s.isDefault(floatKey)) + + assert(s.set(floatKey, floatValue)) + assert(s.get(floatKey) == floatValue) + assert(not s.isDefault(floatKey)) + + -- test with value of bool type + assert(s.get(boolKey) == defaults.printer_useSubLayers.default) + assert(s.isDefault(boolKey)) + + assert(s.set(boolKey, boolValue)) + assert(s.get(boolKey) == boolValue) + assert(not s.isDefault(boolKey)) +end + +function M:test_dotsReplacement() + local underscoredKey, dottedKey, mixedKey = 'printer_retraction_speed', 'printer.retraction.speed', 'printer.retraction_speed' + + assert(s.get(underscoredKey) == defaults.printer_retraction_speed.default) + assert(s.get(dottedKey) == defaults.printer_retraction_speed.default) + assert(s.get(mixedKey) == defaults.printer_retraction_speed.default) + + assert(s.set(mixedKey, 54321)) + assert(s.get(underscoredKey) == 54321) + assert(s.get(dottedKey) == 54321) +end + +function M:test_constraints() + local key, key2 = 'network_ap_address', 'printer_temperature' + local goodValue, badValue1, badValue2 = '10.0.0.1', '10.00.1', '10.0.0d.1' + + assert(s.set(key, goodValue)) + assert(s.set(key, badValue1) == nil) assert(s.get(key) == goodValue) assert(s.set(key, badValue2) == nil) assert(s.get(key) == goodValue) - assert(s.set(key, nil)) - assert(s.isDefault(key)) - - -- test with value of int type - assert(s.get(intKey) == defaults.temperature.default) - assert(s.isDefault(intKey)) - - assert(s.set(intKey, intValue)) - assert(s.get(intKey) == intValue) - assert(not s.isDefault(intKey)) + assert(s.get(key2) == defaults.printer_temperature.default) + assert(s.set(key2, -1) == nil) + assert(s.get(key2) == defaults.printer_temperature.default) end function M:test_setNonExistent() - local fakeKey = 'theAnswer' + local fakeKey = 'theQuestion' assert(s.get(fakeKey) == nil) assert(s.set(fakeKey, 42) == nil) diff --git a/src/util/settings.lua b/src/util/settings.lua index ca13188..4991abc 100644 --- a/src/util/settings.lua +++ b/src/util/settings.lua @@ -1,12 +1,15 @@ --[[ - This settings interface reads and writes its configuration using UCI. + The settings interface reads and writes its configuration using UCI. The corresponding config file is /etc/config/wifibox. To have an initial set of reasonable settings (and allow users to easily return to them), any key not found in the UCI configuration is looked up in the (immutable) 'base configuration' (base_config.lua). This file also contains constraints to check if newly set values are valid. + + By the way, returning correct values in get()/fromUciValue() for booleans has been fixed at a + relatively convenient time purely thanks to the unit tests...just to indicate they are useful. :) ]]-- -local u = require('util.utils') +local utils = require('util.utils') local baseconfig = require('conf_defaults') local uci = require('uci').cursor() @@ -19,15 +22,25 @@ local UCI_CONFIG_SECTION = 'general' -- the section name that will be used in UC local ERR_NO_SUCH_KEY = "key does not exist" -local function toUciValue(v, type) - if type == 'bool' then return v and '1' or '0' end +--- Returns the given key with all periods ('.') replaced by underscores ('_'). +-- @param key The key for which to substitute dots. +-- @return The substituted key, or the key parameter itself if it is not of type 'string'. +local function replaceDots(key) + if type(key) ~= 'string' then return key end + return key:gsub('%.', '_') +end + +local function toUciValue(v, vType) + if vType == 'bool' then return v and '1' or '0' end return tostring(v) end -local function fromUciValue(v, type) - if type == 'bool' then +local function fromUciValue(v, vType) + if v == nil then return nil end + + if vType == 'bool' then return (v == '1') and true or false - elseif type == 'float' or type == 'int' then + elseif vType == 'float' or vType == 'int' then return tonumber(v) else return v @@ -36,20 +49,20 @@ local function fromUciValue(v, type) end local function isValid(value, baseTable) - local type, min, max, regex = baseTable.type, baseTable.min, baseTable.max, baseTable.regex + local varType, min, max, regex = baseTable.type, baseTable.min, baseTable.max, baseTable.regex - if type == 'bool' then - return isboolean(value) or nil,"invalid bool value" + if varType == 'bool' then + return type(value) == 'boolean' or nil,"invalid bool value" - elseif type == 'int' or type == 'float' then + elseif varType == 'int' or varType == 'float' then local numValue = tonumber(value) local ok = numValue and true or false - ok = ok and (type == 'float' or math.floor(numValue) == numValue) + ok = ok and (varType == 'float' or math.floor(numValue) == numValue) if min then ok = ok and numValue >= min end if max then ok = ok and numValue <= max end return ok or nil,"invalid int/float value or out of range" - elseif type == 'string' then + elseif varType == 'string' then local ok = true if min then ok = ok and value:len() >= min end if max then ok = ok and value:len() <= max end @@ -66,7 +79,11 @@ local function getBaseKeyTable(key) end +--- Returns the value of the requested key if it exists. +-- @param key The key to return the associated value for. +-- @return The associated value, beware (!) that this may be boolean false for keys of 'bool' type. function M.get(key) + key = replaceDots(key) local base = getBaseKeyTable(key) if not base then return nil,ERR_NO_SUCH_KEY end @@ -74,21 +91,27 @@ function M.get(key) local v = base.default local uciV = fromUciValue(uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key), base.type) - return uciV or v + local actualV = v + if uciV ~= nil then actualV = uciV end + + return actualV end function M.exists(key) + key = replaceDots(key) return getBaseKeyTable(key) ~= nil end function M.isDefault(key) + key = replaceDots(key) if not M.exists(key) then return nil,ERR_NO_SUCH_KEY end return uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) == nil end -- pass nil as value to restore default function M.set(key, value) - local r = u.create(UCI_CONFIG_FILE) + key = replaceDots(key) + local r = utils.create(UCI_CONFIG_FILE) uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, UCI_CONFIG_TYPE) local base = getBaseKeyTable(key)