--[[ SCRIPT: Widget: DevMon File: devmon.lua Version: 1.6 HISTORY 2023-01-09 V1.6 Channels configuration lines enclosed in expanding panel Name added to flight mode string (1.4.5 and later) 2022-12-20 V1.5 Added relative display mode (tx only, not simulator) Improved auto assignment of control surfaces Refactored code 2022-11-30 V1.4 Fixed numeric channels overflowing drawing area Fixed active LS's not highlighted on low res screens (now with colour background) Long LS labels are truncated Cosmetic improvements 2022-11-19 V1.3 - Fixed run time error if number of timers < 3 Fixed LH panels not drawn 2022-11-06 V1.2 - Fixed first LS of each column not drawn 2022-11-06 V1.1 - Refactored code for channel->surface mapping Title hidden to maximise window height 2022-10-29 V1.0 - First version. AUTHOR Mike Shellim REQUIREMENTS Ethos 1.4.5 or later Ethos transmitter or simulator DESCRIPTION DevMon is a widget which allows you to visualise the mixing scheme of an Ethos setup without the need for a model. Its purpose is to facilitate development, testing and validation of a mixer scheme prior to final adjustments in the Outputs menu. LATEST VERSION AND DOCUMENTATION http://rc-soar.com/ethos/scripts/devmon COPYRIGHT Copyright (c) Michael Shellim 2023 --]] local N_CHANS = 9 -- max number of channels displayed. --[[ Model dimensions (in 'model' units, 1 unit = elevator span) --]] local ELE_LEN = 1 -- reference dimension, other entries are relative. local SPAN_LEN = 3.5 local RUD_LEN = 0.7 local MAX_DEFL = 0.12 --[[ Derived dimensions (in 'model' units) --]] local modelWidth = SPAN_LEN -- width of model local modelHeight = RUD_LEN + 6*MAX_DEFL -- height of model, allowing for three lines of text under wing. -- Origin of model coordinates, relative to top left corner of model rectangle. local modelXOrg = modelWidth/2 local modelYOrg = RUD_LEN + MAX_DEFL -- Other local PADDING = 5 -- padding within model window (pixels) local LONG_PRESS_DURN = 1 -- min long press duration -- Colours local BG_VDARK = lcd.RGB (68, 114, 196) local BG_DARK = lcd.RGB (180, 198, 231) local BG_MED = lcd.RGB (217, 225, 242) local BG_LIGHT = lcd.RGB (237,237,237) local FG_DARK = BLACK local FG_ERROR = RED -- from system.getVersion() local ethosVersion -- MMmmrr local ethosSimulation -- true if running in simulator -- Offsets of momentary switches in CATEGORY_SWITCH_POSITIONS array (determined by experiment) local switchIds = {23, 26, 29, 0} -- SH, SI, SJ, '---' -- Options array, for configure(). Populated in Create() local switchOptions = {} local function isValidSwitchId (switchId) for i = 1, #switchIds do if switchIds [i] == switchId then return true end end return false end --[[ print to stdout (debugging) --]] local function dprint (...) print (...) -- uncomment to debug. end --[[ Get channel name --]] local function getChannelName(ich) local src = system.getSource ({category=CATEGORY_CHANNEL, member=(ich-1), options=0}) local chName = src:name() local pos = string.find (chName, '%(' ) if pos then chName = string.sub(chName, pos+1, -2) -- strip '(' and ')' else chName = '' end return chName end --[[ Get channel aggregated mixer value and scale to +/-100 --]] local function getChannelValue(ich) local src = system.getSource ({category=CATEGORY_CHANNEL, member=(ich-1), options=0}) return math.floor ((src:value() / 10.24) +0.5) end --[[ Display mode (relative/absolute) is controlled by a momentary switch --]] -- States local DS_ABSOLUTE = 0 -- display deflections based on absolute channel values local DS_RELATIVE = 1 -- display deflections relative to user defined base values local dispMode = DS_ABSOLUTE -- default. Altered in button event handler. local chValBaseUnclipped = {} -- channel value offsets for Relative mode. --[[ Event handler for display mode button Returns true to ignore subsequent break events. --]] local EV_BTN_LONG = 1 -- button long press local EV_BTN_BREAK = 2 -- button released local function btnEventHandler (event) local killEventsAfter = false if event == EV_BTN_LONG then if dispMode == DS_ABSOLUTE then -- copy snapshot of channel values into base values -- and change display mode to Relative dispMode = DS_RELATIVE for i = 1, N_CHANS do chValBaseUnclipped [i] = getChannelValue (i) end killEventsAfter = true end elseif event == EV_BTN_BREAK then dispMode = DS_ABSOLUTE end return killEventsAfter end --[[ Button interface btn = button() create button instance poll() call at intervals, detect and process button presses configurable() true if displayed in configure() enabled() true if button is enabled name() return button label getSwitchId() get the switch id setSwitchId() set the switch id Button must be assigned via setSource() before it can be used. --]] local function button () -- state variables local lastVal = nil local lastTime local killEvents = true local buttonSource = nil local switchId = 0 local btnName = nil -- Returns true if button can be shown in configure() screen -- (return false if in simulator) local function configurable () return not ethosSimulation end local function enabled () return configurable () and (switchId ~= 0) end local function name() return btnName end local function getSwitchId() return switchId end local function setSwitchId(swId) switchId = swId lastTime = os.clock() killEvents = true dispMode = DS_ABSOLUTE if swId ~= 0 then buttonSource = system.getSource ({category=CATEGORY_SWITCH_POSITION, member=swId}) btnName = buttonSource:name() lastVal = buttonSource:rawValue() else buttonSource = nil lastVal = nil btnName = '---' end end -- this function is called in wakeup() to poll the display mode button -- calls btnEventhandler() if long or short button press is detected. local function poll () if not enabled() then return end local val = buttonSource:rawValue() if val == 1024 then if lastVal == val then -- Check for long press. if (os.clock() - lastTime >= LONG_PRESS_DURN) and not killEvents then killEvents = btnEventHandler (EV_BTN_LONG) end else -- Must be Off to On transition lastTime = os.clock() killEvents = false end elseif val == -1024 and lastVal ~= val and not killEvents then -- button released btnEventHandler (EV_BTN_BREAK) end lastVal = val end return {poll=poll, name=name, enabled=enabled, configurable=configurable, getSwitchId= getSwitchId, setSwitchId = setSwitchId} end -- define a button to switch display modes local modeButton = button() --[[ Interface for creating and managing boxes within a grid. Usage: box = gridBox () box.create (grid, row-coltable, pxPadding) box.getDrawingarea () box.getGetsize () box.associate (wModel, hModel, xModelOrg, yModelOrg) box.m2d (xMod, yMod) box.fill (color) --]] local function gridBox () local x, y, w, h local scale local xOrg, yOrg local padding -- Create a box given the grid table, and the row and column spans. local function boxCreate (grid, t, pxPadding) -- calculate box coordinates and dimensions in device units x = grid.cols [t.col] y = grid.rows [t.row] w = grid.cols [(t.endcol or t.col) + 1] - grid.cols [t.col] h = grid.rows [(t.endrow or t.row) + 1] - grid.rows [t.row] padding = pxPadding or 0 end -- associate box with a rectangle in model units local function boxAssociate (wModel, hModel, xModelOrg, yModelOrg) -- store parameters for converting model units to device units scale = math.min ((w - 2*padding)/wModel, (h - 2*padding) / hModel) xOrg = x + w/2 + scale * (wModel/2 - xModelOrg) yOrg = y + h/2 - scale * (hModel/2 - yModelOrg) end -- return outer box size (ignoring padding) local function boxGetsize () return x, y, w, h end -- return drawing area in device units, allowing for padding local function boxGetdrawingarea () return x + padding, y + padding, w - 2*padding, h-2*padding end -- convert a point from model units to device units local function boxM2d (xMod, yMod) return xOrg + xMod * scale, yOrg - yMod * scale end -- fill box with given colour local function boxFill (color) local oldColor = lcd.color() lcd.color(color) lcd.drawFilledRectangle (x, y, w, h) lcd.color (oldColor) end return {create = boxCreate, m2d = boxM2d, fill = boxFill, associate = boxAssociate, getdrawingarea = boxGetdrawingarea, getSize = boxGetsize} end -- create gridBox instance local box = gridBox () --[=[ Panel definitions. First six are wing panels with coordinates in panel units Remaining panels are in model units surfaces fields: label: label of surface assigned: true if surface has been assigned to a channel contains: substrings to search in channel name, substrings supplied in table tSubs: {[AND|OR], [[!]substring1|tSubs] ...}. panelNumber: panel offset from root (wing panels only) (vx0, vy0), (vx1, vy1): coordinates of undeflected surface txtPos=0.5 --]=] local AND = 0 local OR = 1 -- index to surfaces table, in order of string search. local LA = 1 local RA = 2 local LF = 3 local RF = 4 local LT = 5 local RT = 6 local EL = 7 local RU = 8 local RV = 9 local LV = 10 local OTH = 11 local surfaces = { [LA] = {label="L Ail", assigned=false, contains={AND, 'ai', {OR, 'left', 'lft', 'lt', '2'}}, panelNumber = 2, vx0=-1, vx1=0, vy0=0, vy1=0, txtPos=0.5}, [RA] = {label="R Ail", assigned=false, contains={AND, 'ai'}, panelNumber = 2, vx0=0, vx1=1, vy0=0, vy1=0, txtPos=0.5}, [LF] = {label="L Flap", assigned=false, contains={AND, 'fl', {OR, 'lt', 'left', 'lft', '2'}}, panelNumber = 1, vx0=-1, vx1=0, vy0=0, vy1=0, txtPos=0.5}, [RF] = {label="R Flap", assigned=false, contains={AND, 'fl'}, panelNumber = 1, vx0=0, vx1=1, vy0=0, vy1=0, txtPos=0.5}, [LT] = {label="L Tip", assigned=false, contains={OR, {AND, 'ail', '4'}, {AND, 'tip', 'l'}}, panelNumber = 3, vx0=-1, vx1=0, vy0=0, vy1=0, txtPos=0.5}, [RT] = {label="R Tip", assigned=false, contains={OR, {AND, 'ail', '3'}, {AND, 'tip'}}, panelNumber = 3, vx0=0, vx1=1, vy0=0, vy1=0, txtPos=0.5}, [EL] = {label="Elev", assigned=false, contains={AND, 'el', '$', '!1', '!2'}, vx0=-ELE_LEN/2, vx1=ELE_LEN/2, vy0=RUD_LEN, vy1=RUD_LEN, txtPos=1, justify=RIGHT}, [RU] = {label="Rudder", assigned=false, contains={AND, 'ru', '$'}, vx0=0, vx1=0, vy0=RUD_LEN, vy1=0, txtPos=0.7, justify=RIGHT}, [RV] = {label="R Vee", assigned=false, contains={OR, {AND, 'elev/rtvee'}, {AND, 'ele', '1'}, {AND, 'v', 'r'}}, vx0=0, vx1=ELE_LEN/3, vy0=0, vy1=RUD_LEN, txtPos=0}, [LV] = {label="L Vee", assigned=false, contains={OR, {AND, 'rudd/ltvee'}, {AND, 'ele', '2'}, {AND, 'v', 'l'}}, vx0=-ELE_LEN/3, vx1=0, vy0=RUD_LEN, vy1=0, txtPos=1, justify=RIGHT}, [OTH]= {label="---", assigned=false, contains={AND, ''}, multi=true}, -- catchall } --[[ This function helps to disambiguate channels with names 'elev/rtvee' and 'rudd/ltvee', which apply to templates by the author which support both V and conventional tails. These are configured according to a VAR channel with name = 'V IsVtail'. Function returns true if channel exists with name = 'V IsVtail' and value ~= 0 Return value is undefined at widget startup - to be effective, user must open Configure menu and choose 'Reinitialise' Note that a linear search is performed - not very efficient. --]] local function hasVtail () for i = 1, 64 do if string.lower (getChannelName (i)) == "v isvtail" then return getChannelValue (i) ~= 0 end end return false end --]] --[[ Seach for substrings supplied in t within target st. Then combine results using boolean operators. First element of t is boolean operator AND/OR. '!' = NOT '$' = is not V-tail Recursively processes operands of type table. --]] local function matchStrings (st, t) local found = false local op = t[1] assert (op == OR or op == AND, 'invalid op') for i = 2, #t do if type (t[i]) == 'table' then found = matchStrings (st, t[i]) else if string.sub (t[i], 1, 1) == '!' then found = not string.find (st, string.sub (t[i], 2)) elseif string.sub (t[i], 1, 1) == '$' then found = not hasVtail() else found = string.find (st, t[i]) end end if (op == OR) and found then break end if (op == AND) and not found then break end end return found end --[[ Assign a surface to each channel based on the channel name --]] local function assignDefaultsurfaces (chAttrs) -- reset assigned flags for i = 1, #surfaces do surfaces[i].assigned = false end -- Step through channels, and attempt to match channel name to a surface -- Surface type 'OTH' is defined as a catchall (last in search order, with multiple assignments allowed and no strings to match). for i = 1, N_CHANS do chAttrs[i] = {} local name = string.lower(getChannelName(i)) -- Step through surfaces searching for a match betweeen the name and the string table for j =1, #surfaces do -- Proceed only if this surface is unassigned or multiple assignments are permitted if not surfaces[j].assigned or surfaces[j].multi then -- Look for a match. -- If found, assign surface to channel, and advance to next channel if matchStrings (name, surfaces [j].contains) then surfaces[j].assigned = true chAttrs[i].surface = j chAttrs[i].dir = 1 break end end end end end --[[ Return height of active font --]] local function getFontheight() local h _, h = lcd.getTextSize (' ') return h end --[[ Return number of entries in a table --]] local function tablelength(t) local count = 0 for _ in pairs(t) do count = count + 1 end return count end --[[ Fix coordinates of surfaces --]] local function updateChannelData (chAttrs) -- Ail, flap, and tipare distributed symmetrically across span -- If a panel pair is not assigned, it collapses. -- Enumerate channels assigned to wing panels, and insert in table chsWithPanel[] -- key is panel number, value is array of channels with that panel number local chsWithPanel = {} for i = 1, N_CHANS do local pn = surfaces [chAttrs[i].surface].panelNumber if pn then if chsWithPanel[pn] == nil then chsWithPanel[pn] = {} end chsWithPanel[pn] [#chsWithPanel[pn] + 1] = i end end -- Calculate lenPanel, such that panels are evenly distributed across span local cntPanels = 2 * tablelength(chsWithPanel) local lenPanel if cntPanels > 0 then lenPanel = SPAN_LEN / cntPanels end -- Calculate coordinates of each wing panel local panelPosFromRoot = 0 for pn = 1,3 do if chsWithPanel [pn] then for _, i in pairs (chsWithPanel[pn]) do local surf = surfaces[chAttrs[i].surface] local isRightSide = surf.vx0 > 0 or surf.vx1 > 0 chAttrs[i].x0 = lenPanel * (surf.vx0 + panelPosFromRoot * (isRightSide and 1 or -1)) chAttrs[i].x1 = lenPanel * (surf.vx1 + panelPosFromRoot * (isRightSide and 1 or -1)) chAttrs[i].y0 = surf.vy0 chAttrs[i].y1 = surf.vy1 end panelPosFromRoot = panelPosFromRoot + 1 end end -- Set coordinates of remaining drawable items for i = 1, N_CHANS do local chAttr = chAttrs[i] local surf = surfaces[chAttr.surface] if not surf.panelNumber and chAttr.surface ~= OTH then chAttr.y0 = surf.vy0 chAttr.y1 = surf.vy1 chAttr.x0 = surf.vx0 chAttr.x1 = surf.vx1 end end -- All drawable items: store normal vector. This is -- used later to calculate deflection lines for i = 1, N_CHANS do local chAttr = chAttrs[i] if chAttr.surface ~= OTH then local xVec = chAttr.x1 - chAttr.x0 local yVec = chAttr.y1 - chAttr.y0 local magnitude = math.sqrt ((xVec * xVec) + (yVec * yVec)) chAttr.xNorm = -yVec / magnitude chAttr.yNorm = xVec / magnitude end end end --[[ Draw line (x0,y0) and (x1, y1): start and end points, in model units pattern: SOLID, DOTTED, DASHED --]] local function drawLine (box, x0, y0, x1, y1, pattern) -- convert to device units x0,y0 = box.m2d (x0, y0) x1,y1 = box.m2d (x1, y1) local oldPen = lcd.pen() if pattern then lcd.pen (pattern) end lcd.drawLine (x0, y0,x1, y1) lcd.pen (oldPen) end --[[ Draw control surfaces --]] local function drawControlSurfaces (chAttrs, box, font, backColor, textColor) box.fill (backColor) lcd.font (font) local hFont = getFontheight() for ich =1, N_CHANS do local chAttr = chAttrs[ich] -- skip if not a control surface if not surfaces[chAttr.surface].vx0 then goto continue end -- calculate deflection vector local chValUnclipped = getChannelValue (ich) -- channel value local chValClipped = math.max (math.min (chValUnclipped, 100), -100) -- clip local chValBaseUnclipped = (dispMode == DS_RELATIVE) and chValBaseUnclipped [ich] or 0 local chValBaseClipped = math.max (math.min (chValBaseUnclipped, 100), -100) -- clip local xDefl = chAttr.dir * MAX_DEFL * chAttr.xNorm * ((chValClipped - chValBaseClipped) / 100) local yDefl = chAttr.dir * MAX_DEFL * chAttr.yNorm * ((chValClipped - chValBaseClipped)/ 100) -- deflection line start and end points local x2 = chAttr.x0 + xDefl local x3 = chAttr.x1 + xDefl local y2 = chAttr.y0 + yDefl local y3 = chAttr.y1 + yDefl -- Colour depends on clipped state local maincolor = (math.abs(chValUnclipped) > 100) and FG_ERROR or textColor -- Draw base and deflection lines. lcd.color (maincolor) drawLine (box, x2, y2, x3, y3, SOLID) -- deflection lcd.color (textColor) drawLine (box, chAttr.x0, chAttr.y0, chAttr.x1, chAttr.y1, DOTTED) -- surface -- close ends drawLine (box, chAttr.x0, chAttr.y0, x2, y2, SOLID) drawLine (box, chAttr.x1, chAttr.y1, x3, y3, SOLID) -- Draw mixer values local pos = surfaces[chAttr.surface].txtPos local flags = surfaces[chAttr.surface].justify or CENTER local x, y x = x2 * pos + x3 * (1-pos) y = y2 * pos + y3 * (1-pos) x,y = box.m2d (x, y) lcd.color (BG_VDARK) lcd.drawText (x, y, "CH" .. ich, flags) -- Display mode = -- relative: draw both rel and abs if different, rel first, abs second line in sq brackets. -- absolute, or relative value == absolute value: draw abs without square brackets. y = y + hFont lcd.color (maincolor) if dispMode == DS_RELATIVE then lcd.drawText (x, y, chValUnclipped - chValBaseUnclipped, flags) if chValBaseUnclipped ~= 0 then y = y + hFont lcd.drawText (x, y, "[" .. chValUnclipped .. "]", flags) end else lcd.drawText (x, y, chValUnclipped, flags) end ::continue:: end -- draw 'left' and 'right' indicators local x,y,w,h = box.getdrawingarea () lcd.color (BG_VDARK) lcd.drawText (x ,y + h - hFont, 'Left', LEFT) lcd.drawText (x + w, y + h - hFont, 'Right', RIGHT) end --[[ Draw timers --]] local function drawTimers (box, font, backColor, textColor) box.fill (backColor) lcd.font (font) local hFont = getFontheight() lcd.color (textColor) local x, y, _, h = box.getdrawingarea () local maxTimers = math.floor (h/hFont) - 1 lcd.drawText (x, y, "<< Timers >>") y = y + hFont for i = 1, maxTimers do local timer = model.getTimer (i-1) if timer == nil then break end local st = timer:name() st = (st ~= '') and st or ('Timer ' .. i) st = st .. ": " .. timer:value() lcd.drawText (x, y, st) y = y + hFont end end --[[ Draw mixer values, for channels not assigned to control surfaces --]] local function drawNumericChannels (chAttrs, title, box, font, backColor, textColor) box.fill (backColor) lcd.font(font) lcd.color (textColor) local hFont = getFontheight() local x, y, _, h = box.getdrawingarea() lcd.drawText (x, y, title) -- max y to draw text local yMax = y + h - hFont -- loop through channels, process only channels of type 'other' -- break once y limit reached. y= y + hFont for i=1, N_CHANS do if y > yMax then break end if chAttrs[i].surface == OTH then local st = "CH".. i .. getChannelName(i) st = string.sub (st, 1, 10) .. ":".. getChannelValue (i) lcd.drawText (x, y, st) y= y + hFont end end end --[[ Draw the active FM number (active FM name is displayed by O/S in the title bar) --]] local function drawFlightMode (box, font, backColor, textColor) box.fill (backColor) local x,y,w, h = box.getdrawingarea () lcd.font (font) local hFont = getFontheight (font) x = x + w/2 y = y + (h-hFont)/2 local src = system.getSource ({category=CATEGORY_FLIGHT_VALUE, member=CURRENT_FLIGHT_MODE}) local stFM = 'FM' .. math.floor (src:value()) -- stringValue () fixed from 1.4.5 if ethosVersion >= 104050 then stFM = stFM .. ":" .. src:stringValue() end lcd.color (textColor) lcd.drawText (x,y,stFM, CENTERED) end --[[ Draw logical switches Returns count of logical switches drawn. --]] local function drawLogicalSwitches (first, title, box, font, backColor, backColortrue) -- Draw the background of the outer box box.fill (backColor) -- draw the title inside box, with left/right padding lcd.font (font) local hFont = getFontheight() local x, y, _, h = box.getdrawingarea () lcd.color (FG_DARK) lcd.drawText (x, y, title or '') y = y + hFont -- get max LS's per column, and index of top entry local max = first + math.floor (h/hFont) - 2 local i = first -- get size of outer rectangle, local wRect, hRect, xRect xRect, _, wRect, hRect = box.getSize() -- Step through LS's while i <= max do local src = system.getSource ({category=CATEGORY_LOGIC_SWITCH, member=i-1, options=0}) if (src == nil) or (src:value() == 0) then break end local bgCol = backColor -- If LS is True, draw text background in different colour if src:value() > 0 then bgCol = backColortrue lcd.color (bgCol) lcd.drawFilledRectangle (xRect, y-1, wRect, hFont) end -- Draw the label lcd.color (BLACK) lcd.drawText (x, y, src:name()) -- Draw the padding column to the left and right of text (looks nicer). lcd.color (bgCol) lcd.drawFilledRectangle (xRect + wRect - PADDING, y-1, PADDING , hFont) -- Advance to next LS y = y + hFont i = i + 1 end return i- first end --[[ Draw status text --]] local function drawStatus (box, font, bgColor, fgColor) -- Fill the panel lcd.font (font) local hFont = getFontheight (font) local x,y,w,h = box.getdrawingarea() box.fill (bgColor) -- Skip if no button defined yet if not modeButton.enabled() then return end -- Draw status info local str local btnName = modeButton.name() if dispMode == DS_ABSOLUTE then str = "Relative mode: long press " .. btnName elseif dispMode == DS_RELATIVE then str = btnName .. " to exit Relative mode." end lcd.color (dispMode == DS_ABSOLUTE and fgColor or FG_ERROR) lcd.drawText (x + w/2, y + (h-hFont)/2, str, CENTERED) end --[[ Called by the O/S to paint the display --]] local function paint(widget) -- Define a grid, with boxes addressed by row, col and spans. local win_w, win_h = lcd.getWindowSize() local grid = { rows={0, math.floor(0.15*win_h), math.floor(0.48*win_h), math.floor(0.7*win_h), math.floor(0.9*win_h), win_h}, cols={0, math.floor(0.16*win_w), math.floor(0.32*win_w), math.floor(0.6*win_w), math.floor(0.80*win_w), win_w} } -- Get number of columns for logical switches (set in widget options). local Lsendcol = widget.nLscols -- Draw logical switches in one or two columns if Lsendcol > 0 then box.create (grid, {row=1, endrow=5, col=1}, PADDING) local nDrawn = drawLogicalSwitches (1, '<< Log sws >>', box, FONT_XS, BG_MED, BG_DARK) if Lsendcol > 1 then box.create (grid, {row=1, endrow=5, col=2}, PADDING) drawLogicalSwitches (nDrawn+1, nil, box, FONT_XS, BG_MED, BG_DARK) end end -- Draw flight mode box.create (grid, {row=1, col=Lsendcol+1, endcol=4}, PADDING) drawFlightMode (box, FONT_L, BG_DARK, FG_DARK) -- Draw control surfaces box.create (grid, {row=2, endrow= (modeButton.enabled() and 4 or 5), col=Lsendcol+1, endcol=4}, PADDING) box.associate (modelWidth, modelHeight, modelXOrg, modelYOrg) drawControlSurfaces(widget.chAttrs, box, FONT_XS, BG_LIGHT, FG_DARK) -- Draw numeric channels box.create (grid, {row=3, endrow=5, col=5}, PADDING) drawNumericChannels (widget.chAttrs, '<< Chans >>', box, FONT_XS, BG_MED, FG_DARK) -- Draw timers box.create (grid, {row=1, endrow=2, col=5}, PADDING) drawTimers (box, FONT_XS, BG_MED, FG_DARK) -- Draw status line if modeButton.enabled() then box.create (grid, {row=5, col=Lsendcol + 1, endcol=4}, PADDING) drawStatus (box, FONT_XS, BG_MED, FG_DARK) end end local function wakeup(widget) modeButton.poll() -- poll for change of display mode lcd.invalidate() end --[[ build list of control surface labels, used in dropdown list in configure menu --]] local function buildLabelList () local res = {} for k, v in ipairs (surfaces) do res[#res + 1] = {v.label, k} end return res end --[[ called by Ethos to display widget options --]] local function configure (widget) local line local rects -- Add input field for LS columns line = form.addLine('Logical switches view') form.addChoiceField(line, nil, {{'Hide',0}, {'1 column',1}, {'2 columns',2}}, function() return widget.nLscols end, function(newValue) widget.nLscols = newValue end ) -- Add picklist for button source if modeButton.configurable () then line = form.addLine("Button to select Relative mode") form.addChoiceField(line, nil, switchOptions, function() return modeButton.getSwitchId() end, function(newValue) widget.btnSwitchId = newValue modeButton.setSwitchId (widget.btnSwitchId) end ) end -- enclose in expansion panel if form.beginExpansionPanel then form.beginExpansionPanel("Assignments") end -- Build table of control surface labels local labels = buildLabelList () -- Build channel options block for i = 1, N_CHANS do line = form.addLine("CH" .. i .. ":" .. getChannelName(i)) rects = form.getFieldSlots(line, {0, ' ',0}) form.addChoiceField( line, rects[1], labels, function() return widget.chAttrs[i].surface end, function(newValue) widget.chAttrs[i].surface = newValue updateChannelData (widget.chAttrs) end ) form.addChoiceField( line, rects[3], {{'normal', 1}, {'inv',-1}}, function() return widget.chAttrs[i].dir end, function(direction) widget.chAttrs[i].dir = direction end ) end if form.endExpansionPanel then form.endExpansionPanel() end --[[ Add line for 'reinitialise' button --]] line = form.addLine('Reinitialise assignments') form.addTextButton(line, nil, "Reinitialise", function() assignDefaultsurfaces(widget.chAttrs) updateChannelData (widget.chAttrs) model:dirty() form.invalidate() end) end --[[ Read widget data from storage, and sanitise --]] local function readwidget(widget) for i = 1, N_CHANS do local surf = storage.read ('surface' .. i) if surf and surfaces [surf] then widget.chAttrs[i].surface = surf local dir = storage.read ('dir' .. i) or 1 widget.chAttrs[i].dir = (dir == 1 or dir == -1) and dir or 1 else -- Error found - assign default surfaces -- and bale out assignDefaultsurfaces(widget.chAttrs) break end end -- Number of columns for drawing LS's local n = storage.read ('nLscols') or 1 widget.nLscols = (n >= 0 and n <= 2) and n or 1 -- button for changing display mode local srcID = storage.read('btnSwitchId') if isValidSwitchId (srcID) then widget.buttonSwitchId = srcID else widget.buttonSwitchId = 0 end modeButton.setSwitchId (widget.buttonSwitchId) -- wrap up updateChannelData (widget.chAttrs) end --[[ write widget data to storage --]] local function writewidget(widget) for i = 1, N_CHANS do storage.write ('surface'..i, widget.chAttrs[i].surface) storage.write ('dir'..i, widget.chAttrs[i].dir) end storage.write ('nLscols', widget.nLscols) storage.write ('btnSwitchId', widget.btnSwitchId) end local function create() local widget = {} -- intialise widget members widget.chAttrs = {} assignDefaultsurfaces(widget.chAttrs) updateChannelData (widget.chAttrs) widget.nLscols = 1 widget.btnSwitchId = 0 modeButton.setSwitchId (widget.btnSwitchId) -- build table of switch options for configure() assert (switchIds [#switchIds] == 0, "last element of switchOptions not zero") for i = 1, (#switchIds - 1) do local src = system.getSource ({category=CATEGORY_SWITCH_POSITION, member=switchIds[i]}) switchOptions [i] = {src:name(), switchIds[i]} end switchOptions [#switchIds] = {'---', 0} -- store version info local v = system.getVersion () ethosVersion = v.major * 10e4 + v.minor * 10e2 + v.revision * 10 ethosSimulation = v.simulation return widget end local function init() system.registerWidget({ key="devmon1", create=create, configure=configure, name= "DevMon", title=false, paint=paint, wakeup=wakeup, read=readwidget, write=writewidget, }) end return {init=init}