From 3957b1220844e66122f97c85d3def66df6cdb7b2 Mon Sep 17 00:00:00 2001 From: Bertrand Songis Date: Mon, 27 Dec 2021 11:27:00 +0100 Subject: [PATCH] More Lua examples --- lua/gauge/gauge.lua | 112 ++++++++++++++++++++++++++++++ lua/servo/servo.lua | 166 ++++++++++++++++++++++++++++++++++++++++++++ lua/servo/servo.png | Bin 0 -> 11356 bytes lua/sinus/sinus.lua | 28 ++++++++ 4 files changed, 306 insertions(+) create mode 100644 lua/gauge/gauge.lua create mode 100644 lua/servo/servo.lua create mode 100644 lua/servo/servo.png create mode 100644 lua/sinus/sinus.lua diff --git a/lua/gauge/gauge.lua b/lua/gauge/gauge.lua new file mode 100644 index 00000000..848f0b67 --- /dev/null +++ b/lua/gauge/gauge.lua @@ -0,0 +1,112 @@ +-- Lua Gauge widget + +local translations = {en="Lua Gauge", fr="Jauge Lua"} + +local function name(widget) + local locale = system.getLocale() + return translations[locale] or translations["en"] +end + +local function create() + return {color=lcd.RGB(0xEA, 0x5E, 0x00), source=nil, min=-1024, max=1024, value=0} +end + +local function paint(widget) + local w, h = lcd.getWindowSize() + + if widget.source == nil then + return + end + + -- Define positions + if h < 50 then + lcd.font(FONT_XS) + elseif h < 80 then + lcd.font(FONT_S) + elseif h > 170 then + lcd.font(FONT_XL) + else + lcd.font(FONT_STD) + end + + local text_w, text_h = lcd.getTextSize("") + local box_top, box_height = text_h, h - text_h - 4 + local box_left, box_width = 4, w - 8 + + -- Source name and value + lcd.drawText(box_left, 0, widget.source:name()) + lcd.drawText(box_left + box_width, 0, widget.source:stringValue(), RIGHT) + + -- Compute percentage + local percent = (widget.value - widget.min) / (widget.max - widget.min) * 100 + if percent > 100 then + percent = 100 + elseif percent < 0 then + percent = 0 + end + + -- Gauge background + gauge_width = math.floor((((box_width - 2) / 100) * percent) + 2) + lcd.color(lcd.RGB(200, 200, 200)) + lcd.drawFilledRectangle(box_left, box_top, box_width, box_height) + + -- Gauge color + lcd.color(widget.color) + + -- Gauge bar + lcd.drawFilledRectangle(box_left, box_top, gauge_width, box_height) + + -- Gauge frame outline + lcd.color(BLACK) + lcd.drawRectangle(box_left, box_top, box_width, box_height) + + -- Gauge percentage + lcd.drawText(box_left + box_width / 2, box_top + (box_height - text_h) / 2, math.floor(percent).."%", CENTERED) +end + +local function wakeup(widget) + if widget.source then + local newValue = widget.source:value() + if widget.value ~= newValue then + widget.value = newValue + lcd.invalidate() + end + end +end + +local function configure(widget) + -- Source choice + line = form.addLine("Source") + form.addSourceField(line, nil, function() return widget.source end, function(value) widget.source = value end) + + -- Color + line = form.addLine("Color") + form.addColorField(line, nil, function() return widget.color end, function(color) widget.color = color end) + + -- Min & Max + line = form.addLine("Range") + local slots = form.getFieldSlots(line, {0, "-", 0}) + form.addNumberField(line, slots[1], -1024, 1024, function() return widget.min end, function(value) widget.min = value end); + form.addStaticText(line, slots[2], "-") + form.addNumberField(line, slots[3], -1024, 1024, function() return widget.max end, function(value) widget.max = value end); +end + +local function read(widget) + widget.source = storage.read("source") + widget.min = storage.read("min") + widget.max = storage.read("max") + widget.color = storage.read("color") +end + +local function write(widget) + storage.write("source", widget.source) + storage.write("min", widget.min) + storage.write("max", widget.max) + storage.write("color", widget.color) +end + +local function init() + system.registerWidget({key="gauge", name=name, create=create, paint=paint, wakeup=wakeup, configure=configure, read=read, write=write}) +end + +return {init=init} diff --git a/lua/servo/servo.lua b/lua/servo/servo.lua new file mode 100644 index 00000000..f2faee0c --- /dev/null +++ b/lua/servo/servo.lua @@ -0,0 +1,166 @@ +-- Lua Servo Configure + +local translations = {en="Lua Servo"} + +local function name(widget) + local locale = system.getLocale() + return translations[locale] or translations["en"] +end + +local requestInProgress = false +local refreshIndex = 0 +local modifications = {} +local repeatTimes = 0 +local fields = {} + +local function getValue(parameter) + if parameter[4] == nil then + return 0 + else + return parameter[4] + end +end + +local function setValue(parameter, value) + parameter[4] = value + modifications[#modifications+1] = {parameter[3], value} + for index = 1, #fields do + fields[index]:enable(false) + end +end + +local function createNumberField(line, parameter) + local field = form.addNumberField(line, nil, parameter[5], parameter[6], function() return getValue(parameter) end, function(value) setValue(parameter, value) end) + field:enableInstantChange(false) + if #parameter == 7 then + field:suffix(parameter[7]) + end + field:enable(false) + return field +end + +local function createChoiceField(line, parameter) + local field = form.addChoiceField(line, nil, parameter[5], function() return getValue(parameter) end, function(value) setValue(parameter, value) end) + field:enable(false) + return field +end + +local function createChannelField(line, parameter) + local field = form.addNumberField(line, nil, parameter[5], parameter[6], function() return getValue(parameter) + 1 end, function(value) setValue(parameter, value - 1) end) + field:prefix("CH") + field:enableInstantChange(false) + field:enable(false) + return field +end + +local function createTextButton(line, parameter) + local field = form.addTextButton(line, nil, parameter[5], function() return setValue(parameter, 0) end) + field:enable(false) + return field +end + +local parameters = { + -- { name, type, default, value, min, max } + {"Physical ID", createNumberField, 0x00, nil, 0, 26 }, + {"Servo ID", createNumberField, 0x01, nil, 0, 15 }, + {"Refresh timer", createNumberField, 0x02, nil, 0, 65535, "ms" }, + {"Range", createChoiceField, 0x04, nil, {{"120°", 0}, {"90°", 1}, {"180°", 2}}}, + {"Direction", createChoiceField, 0x05, nil, {{"CW", 0}, {"CCW", 1}}}, + {"PWM pulse type", createChoiceField, 0x06, nil, {{"1500us", 0}, {"760us", 1}}}, + {"S.Port channel", createChannelField, 0x07, nil, 0, 23 }, + {"Center", createNumberField, 0x08, nil, -125, 125 }, + {"", createTextButton, 0x30, nil, "Save to flash"}, +} + +local function updateField(index) + if parameters[index][4] ~= nil then + fields[index]:enable(true) + end +end + +local function create() + requestInProgress = false + refreshIndex = 0 + modifications = {} + repeatTimes = 0 + fields = {} + + local sensor = sport.getSensor({appIdStart=0x6800, appIdEnd=0x680F}); + + for index = 1, #parameters do + local parameter = parameters[index] + local line = form.addLine(parameter[1]) + local field = parameter[2](line, parameter) + fields[#fields + 1] = field + end + + sensor:idle() + + return {sensor=sensor} +end + +local function wakeup(widget) + if widget.sensor:alive() then + if requestInProgress then + local value = widget.sensor:getParameter() + if value then + local fieldId = value % 256 + local parameter = parameters[refreshIndex + 1] + if fieldId == parameter[3] then + value = math.floor(value / 256) + if parameter[3] == 0x08 then + value = value & 0xFF; + if value > parameter[6] then + value = value - 256 + end + end + + parameters[refreshIndex + 1][4] = value + if value ~= nil then + fields[refreshIndex + 1]:enable(true) + if refreshIndex + 2 == #parameters then + fields[#parameters]:enable(true) + end + end + + refreshIndex = refreshIndex + 1 + requestInProgress = false + end + else + requestInProgress = false + end + else + if #modifications > 0 then + if widget.sensor:writeParameter(modifications[1][1], modifications[1][2]) == true then + if modifications[1][1] == 0x01 then -- appId changed + widget.sensor:appId(0x6800 + modifications[1][2]) + end + refreshIndex = 0 + requestInProgress = false + modifications[1] = nil + end + elseif refreshIndex < (#parameters - 1) then + local parameter = parameters[refreshIndex + 1] + if widget.sensor:requestParameter(parameter[3]) then + requestInProgress = true + end + end + end + end +end + +local function event(widget, category, value, x, y) + print("Event received:", category, value, x, y, KEY_EXIT_BREAK) + if category == EVT_KEY and value == KEY_EXIT_BREAK then + widget.sensor:idle(false) + end + return false +end + +local icon = lcd.loadMask("/scripts/servo.png") + +local function init() + system.registerSystemTool({name=name, icon=icon, create=create, wakeup=wakeup, event=event}) +end + +return {init=init} diff --git a/lua/servo/servo.png b/lua/servo/servo.png new file mode 100644 index 0000000000000000000000000000000000000000..3ce2797e377069ecc07dc5ae5ac4b4c05e82c446 GIT binary patch literal 11356 zcmeHrXH-+$)^?}@(m|RaF@PW~1VZo95s(f61p)~WdWm$T_o7rmKst!@E>%#fRO!7c z9h4$fAfa1(c*AT4@cLzGTI9j6YkU+Gj6B3B@Kv@C+9y6t>wr-5#vB6ia zx2kblWqHF`dt5DVRR26{D~V@=?QQST_v$CB!h8S$;egTItFv#bzoN?Lyq`8G7q{pS z8SFK5w@0mImV5f)XMP*H&^z3AT;9Im-QFgPtEZei`Z=?@eK~N^Oj^EpVH^Lw^-6O1 zLV>QC^ZhTruI|-LK7=OrEe+s1-h|5`0pgdbnMB zyzQq!X zuU0Q%jKb$*XKaNoeqFac9&y63xpvDVLazwc)~9|(er9g{Woi6cWw0(y8o9fAP9n@) z-nwj=R9C_P#CgTp?fG^B3k|-1^C)DbgK9<&ufFl?ca}<4mXRH2N+wJ z@!?GR{oMU^+~r!5OyJa1ME2;}Y6$on{)i*30}IP?bD)uf|Kj4}d+zcG;^f|%YAF8Or_EOs>ZbTKFTVRu9 zP^_D~epjW6x(GCqj;80Sol`AVH?nPsx#Jz!`mj*Ld%NX%dtOl7jm$f9d|lB30}_2D zI+khk1C{etPCX1b1=hJpTwPzw{PSb4)z%K2L#`Qu~-0RUDk|yM2*(Ctu`q1 zv_MhZ$&4h)5dIYR@Q@k5^}QrfCF(2FgSEW*=FThIwePB@$31PS-zL)@NQCxtTg=#Nn@T zr7~KM5m)vveU3@*^Sra#fw^%3R^C;piVu9qSW9VW|Iq7LBp!cSQRg+jFsKPLGrJ>| zrwx(Qrb7+_@L66sMfpU>J+f0ssS?Vpd)@ZolVJx#MSsN^ZDWD2f7R=TTg(;w@(U3; zYM-|X=H4(I%K-0zDqb*`jq-Lr?i(_o{k0r=a!bnAH7GA%97?XZpZQMfmE^3$z<4h< z%o!&CID~aFFK^F3i{w7d65Xi;Od+XfZ(*FB>#bQ^QuRGH&%m^ bWORMv}bTgwR7 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
Sav#f?5IvoPWF+? zq=Cp2>`OS-8YT21xM{d}6tl<->5rXn-@dJv)+!n`U}@{%aE~6u_66U!?UUKkfTh56 z2`?X4^j-dos2AGWlo9eGj0KF%x?1|wowQ&RJLUE(Q86xt7+zl9D(;(YfEL42eN0)$ zQb{y^h9x8<#Kgp4Dglpca(vvGQCM2q^Z>r0XcbUj)Y*79m{o-mH~Xm7q)zA2Bed36 zU;9_oO1WAw&-_O>5wK+JkkM=cqVw~cLshf86(OkteNcgR%7X#Eu9?219R901_7VF;2h*-!BFL#uH@#75?a#j&^o;Ke;&hcGxLd#VyBZTWJWC z>7NIm+K%_7t+jRdPIde}KxKzHB&riT>)D|E zXnzC1f!JhL4F8*xNY&(BtDFh_O_83jc{n!Tlh-|8uj`F|PKi>F$V>qpEY+)a2PNR` zs7*tcManRZ`nC(ld7`H!)jT6wo4V5)T31{9nLQe^P-zjydBaoSXNjsD3t!j0gotOu zMI}w!BAH(Vh;>>mb~>GDaz#h8)L3)v`5kYqXxt6xMVP~qY33ia^rwL?k#ynEH9;lQ~iheKj0ZNO6O#&D@be&B1;L@S4G+7`$!aq%s& zrqq}{m7-W28rm?;g)J2;S*V3jT6MSL%JSggV7!&mmY$Vp#$a+X&AlO6eg!Q%A>%WN zI~u0-DxhE)zjcW_J}FQ)(bFbKz)% z-_^6Ju`CbcPVxMWt9*0%NC`#>kBXL-jNV2fth;0L^YaS}q~d1C2D@m=J>5i#-~f1Z z+N)P(Pnz_Njf;)eCMG6|4a#TiguIk5#GlcyY**xI7u_Jc0~Hjc__ejOgQ;AVeC;m_ z13Nj5q18)I3#Q98d+Y6m1Nsm!8U+YBL9_P>FCDSNVrxrvb#+}G%>m=hg;C9Bp;VsrN)lJI{%f&S