Skip to content

Instantly share code, notes, and snippets.

@cheapie
Created November 27, 2021 03:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cheapie/2039ae3274e7d629adf377b7777520d1 to your computer and use it in GitHub Desktop.
Save cheapie/2039ae3274e7d629adf377b7777520d1 to your computer and use it in GitHub Desktop.
--Light Controller 2
--A product of Advanced Mesecons Devices, a Cheapie Systems company
--This is free and unencumbered software released into the public domain.
--See http://unlicense.org/ for more information
local MODE_OFF = 0
local MODE_ON = 1
local MODE_DUSKDAWN = 2
local MODE_DAWNDUSK = 3
local MODE_GAMETIME = 4
local MODE_REALTIME = 5
local MODE_INTERNET = 6
local INITSTATE_POWERON = 0
local INITSTATE_HWDETECT_BLINKY = 1
local INITSTATE_HWDETECT_LS = 2
local INITSTATE_HWDETECT_RTC = 3
local INITSTATE_HWDETECT_NIC = 4
local INITSTATE_SUMMARY = 5
local INITSTATE_READY = 6
local SCREENSTATE_INIT = 0
local SCREENSTATE_MAIN = 1
local SCREENSTATE_STATUS = 2
local SCREENSTATE_MODE = 3
local SCREENSTATE_SETTINGS = 4
local SCREENSTATE_ABOUT = 5
local modestrings = {
[MODE_OFF] = "Always Off",
[MODE_ON] = "Always On",
[MODE_DUSKDAWN] = "Dusk to Dawn",
[MODE_DAWNDUSK] = "Dawn to Dusk",
[MODE_GAMETIME] = "In-Game Time",
[MODE_REALTIME] = "Real-Life Time",
[MODE_INTERNET] = "HTTP Remote",
}
local function describemode()
local ret = modestrings[mem.mode]
if mem.mode == MODE_DUSKDAWN then
return ret.."\\, on when light level is below "..mem.lighttrig
elseif mem.mode == MODE_DAWNDUSK then
return ret.."\\, on when light level is above "..mem.lighttrig
elseif mem.mode == MODE_GAMETIME or mem.mode == MODE_REALTIME then
return string.format("%s\\, on from %02d\\:%02d to %02d\\:%02d",ret,mem.ontime.hour,mem.ontime.min,mem.offtime.hour,mem.offtime.min)
else
return ret
end
end
local fs = "size[12,8]formspec_version[4]"
local function fsadd(element)
fs = fs..element
end
local function formatgametime(rtcoutput)
local ret = {}
rtcoutput = rtcoutput*24
ret.hour = math.floor(rtcoutput)
ret.min = math.floor((rtcoutput-math.floor(rtcoutput))*60)
return ret
end
local function istimeinrange(current,start,stop)
local currhour = current.hour+(current.min/60)
local starthour = start.hour+(start.min/60)
local stophour = stop.hour+(stop.min/60)
if stophour > starthour then
return (currhour >= starthour and currhour < stophour)
elseif stophour < starthour then
return (currhour >= starthour or currhour < stophour)
else
return false
end
end
local function getdesiredstate()
if mem.mode == MODE_OFF then
return false
elseif mem.mode == MODE_ON then
return true
elseif mem.mode == MODE_DUSKDAWN then
return mem.lightlevel < mem.lighttrig
elseif mem.mode == MODE_DAWNDUSK then
return mem.lightlevel > mem.lighttrig
elseif mem.mode == MODE_GAMETIME then
return istimeinrange(formatgametime(mem.gametime),mem.ontime,mem.offtime)
elseif mem.mode == MODE_REALTIME then
return istimeinrange(os.datetable(),mem.ontime,mem.offtime)
elseif mem.mode == MODE_INTERNET then
return mem.url and mem.niccommand
end
end
if event.type == "program" or (mem.screenstate == SCREENSTATE_SETTINGS and event.channel == "touchscreen" and event.msg.defaults) then
mem.mode = MODE_OFF
mem.initstate = INITSTATE_POWERON
mem.screenstate = SCREENSTATE_INIT
mem.blinkypin = nil
mem.lsfound = false
mem.rtcfound = false
mem.nicfound = false
mem.url = nil
mem.lightstate = false
mem.ontime = {hour = 0,min = 0}
mem.offtime = {hour = 0,min = 0}
mem.lighttrig = 7
mem.brightness = 14
mem.offbrightness = 0
elseif event.channel == "touchscreen" then
local fields = event.msg
if mem.screenstate == SCREENSTATE_INIT then
if mem.initstate == INITSTATE_POWERON and fields.next then
mem.initstate = INITSTATE_HWDETECT_BLINKY
interrupt(5,"blinkydetect")
elseif mem.initstate == INITSTATE_SUMMARY then
if fields.back then
mem.blinkypin = nil
mem.lsfound = false
mem.rtcfound = false
mem.nicfound = false
mem.initstate = INITSTATE_POWERON
elseif fields.next then
mem.initstate = INITSTATE_READY
mem.screenstate = SCREENSTATE_MAIN
end
end
elseif mem.screenstate == SCREENSTATE_MAIN then
if fields.viewstatus then
mem.screenstate = SCREENSTATE_STATUS
elseif fields.about then
mem.screenstate = SCREENSTATE_ABOUT
elseif fields.editsettings then
mem.screenstate = SCREENSTATE_SETTINGS
elseif fields.selectmode then
mem.screenstate = SCREENSTATE_MODE
end
elseif mem.screenstate == SCREENSTATE_SETTINGS then
if fields.cancel then
mem.screenstate = SCREENSTATE_MAIN
elseif fields.save then
mem.screenstate = SCREENSTATE_MAIN
fields.onhour = tonumber(fields.onhour) and math.floor(tonumber(fields.onhour))
fields.onmin = tonumber(fields.onmin) and math.floor(tonumber(fields.onmin))
fields.offhour = tonumber(fields.offhour) and math.floor(tonumber(fields.offhour))
fields.offmin = tonumber(fields.offmin) and math.floor(tonumber(fields.offmin))
fields.brightness = tonumber(fields.brightness) and math.floor(tonumber(fields.brightness))
fields.offbrightness = tonumber(fields.offbrightness) and math.floor(tonumber(fields.offbrightness))
fields.triggerlevel = tonumber(fields.triggerlevel) and math.floor(tonumber(fields.triggerlevel))
if fields.onhour and fields.onhour >= 0 and fields.onhour <= 23 then
mem.ontime.hour = fields.onhour
end
if fields.onmin and fields.onmin >= 0 and fields.onmin <= 59 then
mem.ontime.min = fields.onmin
end
if fields.offhour and fields.offhour >= 0 and fields.offhour <= 23 then
mem.offtime.hour = fields.offhour
end
if fields.offmin and fields.offmin >= 0 and fields.offmin <= 59 then
mem.offtime.min = fields.offmin
end
if fields.triggerlevel and fields.triggerlevel >= 0 and fields.triggerlevel <= 14 then
mem.lighttrig = fields.triggerlevel
end
if fields.brightness and fields.brightness >= 0 and fields.brightness <= 14 then
mem.brightness = fields.brightness
end
if fields.offbrightness and fields.offbrightness >= 0 and fields.offbrightness <= 14 then
mem.offbrightness = fields.offbrightness
end
if fields.url then
mem.url = (string.len(fields.url) > 0) and fields.url
end
end
elseif mem.screenstate == SCREENSTATE_STATUS and fields.back then
mem.screenstate = SCREENSTATE_MAIN
elseif mem.screenstate == SCREENSTATE_ABOUT and fields.back then
mem.screenstate = SCREENSTATE_MAIN
elseif mem.screenstate == SCREENSTATE_MODE then
if fields.back then
mem.screenstate = SCREENSTATE_MAIN
elseif fields.mode then
if string.sub(fields.mode,1,3) == "CHG" then
local newmode = tonumber(string.sub(fields.mode,5,-1))
local modes = {MODE_OFF,MODE_ON}
if mem.blinkypin then
if mem.lsfound then
table.insert(modes,MODE_DUSKDAWN)
table.insert(modes,MODE_DAWNDUSK)
end
if mem.rtcfound then
table.insert(modes,MODE_GAMETIME)
end
table.insert(modes,MODE_REALTIME)
if mem.nicfound then
table.insert(modes,MODE_INTERNET)
end
end
mem.mode = modes[newmode]
end
end
end
elseif (event.type == "on" or event.type == "off") and mem.initstate == INITSTATE_HWDETECT_BLINKY then
mem.blinkypin = event.pin.name
elseif event.iid == "blinkydetect" and mem.initstate == INITSTATE_HWDETECT_BLINKY then
mem.initstate = INITSTATE_HWDETECT_LS
digiline_send("sensor","GET")
interrupt(1,"lsdetect")
elseif mem.initstate == INITSTATE_HWDETECT_LS and event.channel == "sensor" then
mem.lsfound = true
mem.lightlevel = 0
elseif event.iid == "lsdetect" and mem.initstate == INITSTATE_HWDETECT_LS then
mem.initstate = INITSTATE_HWDETECT_RTC
digiline_send("rtc","GET")
interrupt(1,"rtcdetect")
elseif mem.initstate == INITSTATE_HWDETECT_RTC and event.channel == "rtc" then
mem.rtcfound = true
mem.gametime = 0
elseif event.iid == "rtcdetect" and mem.initstate == INITSTATE_HWDETECT_RTC then
mem.initstate = INITSTATE_HWDETECT_NIC
digiline_send("nic","http://clients1.google.com/generate_204")
interrupt(1,"nicdetect")
elseif mem.initstate == INITSTATE_HWDETECT_NIC and event.channel == "nic" then
mem.nicfound = true
mem.niccommand = false
elseif event.iid == "nicdetect" and mem.initstate == INITSTATE_HWDETECT_NIC then
mem.initstate = INITSTATE_SUMMARY
elseif mem.initstate == INITSTATE_READY and mem.blinkypin and (event.type == "on" or event.type == "off") then
if mem.nicfound and mem.url then digiline_send("nic",mem.url) end
if mem.lsfound then digiline_send("sensor","GET") end
if mem.rtcfound then digiline_send("rtc","GET") end
elseif mem.initstate == INITSTATE_READY and event.channel == "nic" then
mem.niccommand = string.sub(event.msg.data,1,2) == "on"
elseif mem.initstate == INITSTATE_READY and event.channel == "sensor" then
mem.lightlevel = event.msg
elseif mem.initstate == INITSTATE_READY and event.channel == "rtc" then
mem.gametime = event.msg
end
if mem.initstate == INITSTATE_READY then
local desstate = getdesiredstate()
port = {}
for _,i in ipairs({"a","b","c","d"}) do
port[i] = desstate and ((not mem.blinkypin) or string.lower(mem.blinkypin) ~= i)
end
digiline_send("light",desstate and mem.brightness or mem.offbrightness)
mem.lightstate = desstate
else
port = {}
digiline_send("light",0)
mem.lightstate = false
end
if mem.screenstate == SCREENSTATE_INIT then
fsadd("label[0,0;Hardware Setup Wizard]")
if mem.initstate == INITSTATE_POWERON then
fsadd("label[0,1;Thank you for choosing Light Controller 2]")
fsadd("label[0,1.5;by Advanced Mesecons Devices\\, a Cheapie Systems company.]")
fsadd("label[0,3;This wizard will help you configure your new light controller.]")
fsadd("label[0,3.5;Click next to continue.]")
fsadd("image_button[11,7;1,1;digistuff_adwaita_go-next.png;next;]")
elseif mem.initstate == INITSTATE_HWDETECT_BLINKY then
fsadd("label[0,1;Detecting hardware, please wait...]")
fsadd("label[0,1.5;Blinky Plant]")
elseif mem.initstate == INITSTATE_HWDETECT_LS then
fsadd("label[0,1;Detecting hardware, please wait...]")
fsadd("label[0,1.5;Light Sensor]")
elseif mem.initstate == INITSTATE_HWDETECT_RTC then
fsadd("label[0,1;Detecting hardware, please wait...]")
fsadd("label[0,1.5;Real Time Clock]")
elseif mem.initstate == INITSTATE_HWDETECT_NIC then
fsadd("label[0,1;Detecting hardware, please wait...]")
fsadd("label[0,1.5;NIC]")
elseif mem.initstate == INITSTATE_SUMMARY then
fsadd("label[0,1;The following hardware was detected\\:]")
fsadd("label[0,2;Blinky Plant: "..(mem.blinkypin and ("Yes\\, pin "..mem.blinkypin.."]") or "No]"))
fsadd("label[0,2.5;Light Sensor (channel 'sensor'): "..(mem.lsfound and "Yes" or "No").."]")
fsadd("label[0,3;Real Time Clock (channel 'rtc'): "..(mem.rtcfound and "Yes" or "No").."]")
fsadd("label[0,3.5;NIC (channel 'nic'): "..(mem.nicfound and "Yes" or "No").."]")
fsadd("label[0,6;If this looks correct\\, press next.]")
fsadd("label[0,6.5;Otherwise\\, press back to try again.]")
local usablemodes = {"Always Off","Always On"}
local unusablemodes = {}
if mem.blinkypin then
table.insert(usablemodes,"Real-Life Time")
if mem.lsfound then
table.insert(usablemodes,"Dusk to Dawn")
table.insert(usablemodes,"Dawn to Dusk")
else
table.insert(unusablemodes,"Dusk to Dawn (light sensor)")
table.insert(unusablemodes,"Dawn to Dusk (light sensor)")
end
if mem.rtcfound then
table.insert(usablemodes,"In-Game Time")
else
table.insert(unusablemodes,"In-Game Time (RTC)")
end
if mem.nicfound then
table.insert(usablemodes,"HTTP Remote")
else
table.insert(unusablemodes,"HTTP Remote (NIC)")
end
else
table.insert(unusablemodes,"All Automatic Modes (blinky plant)")
end
local usablemodestr = ""
for _,i in ipairs(usablemodes) do usablemodestr = usablemodestr..i.."\\, " end
usablemodestr = string.sub(usablemodestr,1,-4)
local unusablemodestr = ""
for _,i in ipairs(unusablemodes) do unusablemodestr = unusablemodestr..i.."\\, " end
unusablemodestr = string.sub(unusablemodestr,1,-4)
if #unusablemodes == 0 then
usablemodestr = "All Modes"
unusablemodestr = "None"
end
fsadd("label[0,4.5;Usable Modes\\: "..usablemodestr.."]")
fsadd("label[0,5;Modes Missing Hardware\\: "..unusablemodestr.."]")
fsadd("image_button[11,7;1,1;digistuff_adwaita_go-next.png;next;]")
fsadd("image_button[10,7;1,1;digistuff_adwaita_go-previous.png;back;]")
end
elseif mem.screenstate == SCREENSTATE_MAIN then
fsadd("label[0,0;Main Menu]")
fsadd(string.format("label[0,1;Lights are currently\\: %s]",mem.lightstate and "ON" or "OFF"))
fsadd(string.format("label[0,1.5;Mode\\: %s]",describemode()))
fsadd("button[0,2.5;2,1;viewstatus;View Status]")
fsadd("button[0,3.5;2,1;selectmode;Select Mode]")
fsadd("button[0,4.5;2,1;editsettings;Edit Settings]")
fsadd("button[0,7;2,1;about;About]")
elseif mem.screenstate == SCREENSTATE_STATUS then
fsadd("label[0,0;System Status]")
fsadd(string.format("label[0,1;Mode\\: %s]",modestrings[mem.mode]))
fsadd(string.format("label[0,1.5;Output\\: %s]",mem.lightstate and "ON" or "OFF"))
local lpos = 2
if mem.lsfound and mem.blinkypin then
fsadd(string.format("label[0,%1.1f;Light level\\: %s]",lpos,mem.lightlevel))
lpos = lpos + 0.5
end
if mem.rtcfound and mem.blinkypin then
local gametime = formatgametime(mem.gametime)
fsadd(string.format("label[0,%1.1f;In-game time\\: %02d\\:%02d]",lpos,gametime.hour,gametime.min))
lpos = lpos + 0.5
end
if mem.nicfound and mem.url and mem.blinkypin then
fsadd(string.format("label[0,%1.1f;HTTP remote command\\: %s]",lpos,mem.niccommand and "ON" or "OFF"))
lpos = lpos + 0.5
end
local datetable = os.datetable()
if mem.blinkypin then
fsadd(string.format("label[0,%1.1f;Real-life time\\: %02d\\:%02d]",lpos,datetable.hour,datetable.min))
end
fsadd("button[0,7;2,1;back;Main Menu]")
elseif mem.screenstate == SCREENSTATE_ABOUT then
fsadd("label[0,0;About]")
fsadd("label[0,1;Light Controller 2 by Advanced Mesecons Devices\\,]")
fsadd("label[0,1.5;a Cheapie Systems company.]")
fsadd("label[0,2.5;This is free and unencumbered software released into the public domain.]")
fsadd("label[0,3;See https\\://unlicense.org/ for the full license text.]")
fsadd("button[0,7;2,1;back;Main Menu]")
elseif mem.screenstate == SCREENSTATE_SETTINGS then
fsadd("label[0,0;Edit Settings]")
fsadd("button[0,7;2,1;save;Save]")
fsadd("button[2,7;2,1;cancel;Cancel]")
fsadd("button[10,7;2,1;defaults;Restore Defaults]")
fsadd("label[0,0.75;Times (for game time and real-life time modes)]")
fsadd(string.format("field[0.25,1.5;0.75,1;onhour;;%02d]",mem.ontime.hour))
fsadd("label[0.65,1.4;\\:]")
fsadd(string.format("field[1.15,1.5;0.75,1;onmin;;%02d]",mem.ontime.min))
fsadd("label[1.55,1.4;—]")
fsadd(string.format("field[2.15,1.5;0.75,1;offhour;;%02d]",mem.offtime.hour))
fsadd("label[2.55,1.4;\\:]")
fsadd(string.format("field[3.05,1.5;0.75,1;offmin;;%02d]",mem.offtime.min))
fsadd(string.format("field[0.25,3;2,1;triggerlevel;Trigger Level;%d]",mem.lighttrig))
fsadd(string.format("field[0.25,4;2,1;brightness;Brightness (on);%d]",mem.brightness))
fsadd(string.format("field[0.25,5;2,1;offbrightness;Brightness (off);%d]",mem.offbrightness))
local urlesc = ""
if mem.url then
local badchars = {
["\\"] = true,
["["] = true,
["]"] = true,
[";"] = true,
[","] = true
}
for i=1,string.len(mem.url),1 do
local char = string.sub(mem.url,i,i)
if badchars[char] then urlesc = urlesc.."\\" end
urlesc = urlesc..char
end
else
urlesc = ""
end
fsadd(string.format("field[0.25,6;4,1;url;Remote Control URL;%s]",urlesc))
elseif mem.screenstate == SCREENSTATE_MODE then
fsadd("label[0,0;Select Mode]")
fsadd("button[0,7;2,1;back;Main Menu]")
local modes = {"Always Off","Always On"}
if mem.blinkypin then
if mem.lsfound then
table.insert(modes,"Dusk to Dawn")
table.insert(modes,"Dawn to Dusk")
end
if mem.rtcfound then
table.insert(modes,"In-Game Time")
end
table.insert(modes,"Real-Life Time")
if mem.nicfound then
table.insert(modes,"HTTP Remote")
end
end
local modesrev = {}
for k,v in ipairs(modes) do modesrev[v] = k end
local sel = 0
if mem.mode == MODE_OFF then sel = modesrev["Always Off"]
elseif mem.mode == MODE_ON then sel = modesrev["Always On"]
elseif mem.mode == MODE_DUSKDAWN then sel = modesrev["Dusk to Dawn"]
elseif mem.mode == MODE_DAWNDUSK then sel = modesrev["Dawn to Dusk"]
elseif mem.mode == MODE_GAMETIME then sel = modesrev["In-Game Time"]
elseif mem.mode == MODE_REALTIME then sel = modesrev["Real-Life Time"]
elseif mem.mode == MODE_INTERNET then sel = modesrev["HTTP Remote"] end
if not sel then sel = 1 end
local modestr = ""
for _,i in pairs(modes) do
modestr = modestr..i..","
end
modestr = string.sub(modestr,1,-2)
fsadd(string.format("textlist[0.25,1;6,6;mode;%s;%d;false]",modestr,sel))
fsadd(string.format("label[7,1;%s]",modestrings[mem.mode]))
if mem.mode == MODE_OFF then
fsadd("label[7,2;Output is always off.]")
elseif mem.mode == MODE_ON then
fsadd("label[7,2;Output is always on.]")
elseif mem.mode == MODE_DUSKDAWN then
fsadd("label[7,2;Output is on when the light level]")
fsadd("label[7,2.5;is below the set threshold.]")
elseif mem.mode == MODE_DAWNDUSK then
fsadd("label[7,2;Output is on when the light level]")
fsadd("label[7,2.5;is above the set threshold.]")
elseif mem.mode == MODE_GAMETIME then
fsadd("label[7,2;Output turns on at the set on time]")
fsadd("label[7,2.5;and off at the set off time.]")
fsadd("label[7,3;Uses in-game time.]")
elseif mem.mode == MODE_REALTIME then
fsadd("label[7,2;Output turns on at the set on time]")
fsadd("label[7,2.5;and off at the set off time.]")
fsadd("label[7,3;Uses real-life time.]")
elseif mem.mode == MODE_INTERNET then
fsadd("label[7,2;Output is on when the file at the]")
fsadd("label[7,2.5;specified URL starts with \"on\".]")
end
end
digiline_send("touchscreen",fs)
@BuckarooBanzay
Copy link

Nice 👍
Judging from the code it needs these devices, right?

  • RTC on channel rtc
  • Light sensor on sensor
  • Digistuff touchscreen on touchscreen
  • HTTP NIC on nic
  • And dimmable lights on light

I'd like to add stuff like this in my mesecons_lab game https://content.minetest.net/packages/BuckarooBanzay/mesecons_lab/ (tutorial/examples game) let me know if thats alright with you (i'll credit you accordingly)

@cheapie
Copy link
Author

cheapie commented Nov 27, 2021

It uses the advanced touchscreen (not the standard one), but yes. It also expects a blinky plant on any pin.
The RTC, light sensor, NIC, and blinky plant are all technically optional (it detects what is connected during startup), but functionality is limited if not all of them are present:

  • Always On and Always Off modes don't need any extra parts
  • Real-Life Time mode requires a blinky plant
  • In-Game Time mode requires a blinky plant and an RTC
  • Dusk to Dawn and Dawn to Dusk modes require a blinky plant and a light sensor
  • HTTP Remote mode requires a blinky plant and a NIC
    Mesecons output is also supported, output is on every pin except the pin the blinky plant is connected to.
    Homedecor lights (glowlights and such) can also be controlled over digilines, and the protocol is compatible with the ones from digistuff.

As mentioned both at the start of the program and on the about screen, you can do anything you want with this program, and you don't have to give me credit for it either. Credit is, however, of course appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment