It is currently March 28th, 2024, 11:11 pm

Persistent data

Tips and Tricks from the Rainmeter Community
skwerlman
Posts: 6
Joined: May 15th, 2014, 6:32 pm

Persistent data

Post by skwerlman »

I came up with a way to cache data so it can be recalled after a skin is refreshed.

I wrote a basic example that measures the ping time for two sites (google and 8.8.8.8) and remembers the highest recorded ping time, which it uses to determine the percent values used by the bar measures.

The example code will work if you add it to the Illustro folder, or if you use the .rmskin installer.

.ini File:

Code: Select all


[Rainmeter]
Author=skwerlman
AppVersion=2003000
Update=1000
Background=#@#Background.png
BackgroundMode=3
BackgroundMargins=0,34,0,14

[Metadata]
Description=Example implementation of data caching
License=GPLv3
Version=1.0.1

[Variables]
fontName=Trebuchet MS
textSize=8
colorBar=235,170,0,255
colorText=255,255,255,205
errorText=255,0,0,205

; This is the name of the cache file.
; It will be created as #@##cachPath# by the script, if it doesn't already exist
cachePath=Ping.cache

; This is set by measureCacheScript
PingMax=1

; the sites to measure
Address1=www.google.com
; google
Address2=8.8.8.8
; common DNS

;-------------------------------------------

; This script manages the cache and updates PingMax accordingly
; It returns 1 under normal operation, or <1 on error
[measureCacheScript]
Measure=Script
ScriptFile=Cache.lua
IfEqualValue=1
IfEqualAction=!HideMeterGroup "cacheError"
IfBelowValue=1
IfBelowAction=!ShowMeterGroup "cacheError"

; This group is responsible for detecting whether the computer was just turned on
; If it was, we execute the flush() function
[measureHours]
Measure=Uptime
Group=ClearCacheOnReboot
AddDaysToHours=1
Format=%3!i!

[measureMinutes]
Measure=Uptime
Group=ClearCacheOnReboot
Format=%2!i!

[measureSeconds]
Measure=Uptime
Group=ClearCacheOnReboot
Format=%1!i!

[measureTimeAsInt]
Measure=Calc
Group=ClearCacheOnReboot
; Convert uptime to seconds
Formula=(measureHours * 360) + (measureMinutes * 60) + measureSeconds
; Check if the uptime is less than 300000 units of time (its not exactly ms..?) (~4m30s)
; if yes, then try to flush the cache
IfBelowValue=300000
IfBelowAction=!CommandMeasure "measureCacheScript" "flush()"

[measurePing1]
Measure=Plugin
Plugin=PingPlugin
DestAddress=#Address1#
MaxValue=#PingMax#
DynamicVariables=1

[measurePing2]
Measure=Plugin
Plugin=PingPlugin
DestAddress=#Address2#
MaxValue=#PingMax#
DynamicVariables=1

;-------------------------------------------

; This example uses the same style code as Illustro by poiru
[styleTitle]
StringAlign=CENTER
StringCase=UPPER
StringStyle=BOLD
StringEffect=SHADOW
FontEffectColor=0,0,0,50
FontColor=#colorText#
FontFace=#fontName#
FontSize=10
AntiAlias=1
ClipString=1

[styleLeftText]
StringAlign=LEFT
StringCase=NONE
StringStyle=BOLD
StringEffect=SHADOW
FontEffectColor=0,0,0,20
FontColor=#colorText#
FontFace=#fontName#
FontSize=#textSize#
AntiAlias=1
ClipString=1

[styleRightText]
StringAlign=RIGHT
StringCase=NONE
StringStyle=BOLD
StringEffect=SHADOW
FontEffectColor=0,0,0,20
FontColor=#colorText#
FontFace=#fontName#
FontSize=#textSize#
AntiAlias=1
ClipString=1

; This is used when we detect a handled script error (one that was detectable)
[styleLeftTextError]
StringAlign=LEFT
StringCase=NONE
StringStyle=BOLD
StringEffect=SHADOW
FontEffectColor=0,0,0,20
FontColor=#errorText#
FontFace=#fontName#
FontSize=#textSize#
AntiAlias=1
ClipString=1

[styleBar]
BarColor=#colorBar#
BarOrientation=HORIZONTAL
SolidColor=255,255,255,15

;-------------------------------------------

[meterTitle]
Meter=STRING
MeterStyle=styleTitle
MeasureName=measureTime
X=100
Y=12
W=190
H=18
Text="Ping"

[meterLabel1]
Meter=STRING
MeterStyle=styleLeftText
X=10
Y=40
W=190
H=14
Text="#Address1#"

[meterPing1]
Meter=STRING
MeterStyle=styleRightText
MeasureName=measurePing1
X=200
Y=0r
W=190
H=14
Text="%1"

[meterBar1]
Meter=BAR
MeterStyle=styleBar
MeasureName=measurePing1
X=10
Y=12r
W=190
H=1

[meterLabel2]
Meter=STRING
MeterStyle=styleLeftText
X=10
Y=8r
W=190
H=14
Text="#Address2#"

[meterPing2]
Meter=STRING
MeterStyle=styleRightText
MeasureName=measurePing2
X=200
Y=0r
W=190
H=14
Text="%1"

[meterBar2]
Meter=BAR
MeterStyle=styleBar
MeasureName=measurePing2
X=10
Y=12r
W=190
H=1

; Gets enabled/disabled by measureCacheScript
[meterErrorLabel]
Meter=String
Group=cacheError
MeterStyle=styleLeftTextError
X=10
Y=8r
W=190
H=14
Text="Errors occured! Check the log."
Lua Script:

Code: Select all


--[[

  This script creates and manages a cache file.
  It will need to be edited before it will work in other skins.

  If you want to use more than 10% of this script in a released skin,
   I ask that you credit me in the derivitive.

]]


-- This is used to track errors.
-- It gets set to 0 on error (or lower on subsequent errors).
-- Because it's outside of Initialize(), it gets reset when any function is called.
local isOK = 1

-- This function writes an error message to the log (as DEBUG)
--  then sets isOK to 0 or lower to indicate an error.
local function printError(msg)
  print(msg)
  isOK = isOK - 1
end

-- This function works similarly to assert(), except it doesn't stop execution
-- It checks whether condition is nil or false. (Well, kinda sorta. It really checks for everything else, but that's harder to word.)
-- If not, it returns true, otherwise it calls printError() using the supplied message.
local function checkOK(condition, msg)
  if condition then return true end
  printError(msg)
end

--[[

  I'm not going to document dan200's functions with any detail, since they use
   recursion and tracking and they aren't exactly relevent to caching.
  
  Basically, they turn (almost) any (non-recursive) table (with no functions) into a string.

  You can replace his serializer/unserializer with any one you choose.
  There are better ones out there, I'm sure. (I'm pretty sure theoriginalbit made one)
  Just make sure that you're writing strings to the cache (not tables, bools, numbers)
   and reading them into tables.

]]

-- This is used by serialize to turn a table into a string.
-- It was written by dan200 for ComputerCraft (a Minecraft mod)
-- http://www.computercraft.info/
local function serializeImpl( t, tTracking )  
  local sType = type(t)
  if sType == "table" then
    if tTracking[t] ~= nil then
      error( "Cannot serialize table with recursive entries" )
    end
    tTracking[t] = true
    local result = "{"
    for k,v in pairs(t) do
      result = result..("["..serializeImpl(k, tTracking).."]="..serializeImpl(v, tTracking)..",")
    end
    result = result.."}"
    return result
  elseif sType == "string" then
    return string.format( "%q", t )
  elseif sType == "number" or sType == "boolean" or sType == "nil" then
    return tostring(t)
  else
    error( "Cannot serialize type "..sType )
  end
end

-- This function converts a table into a string.
-- It was written by dan200 for ComputerCraft (a Minecraft mod)
-- http://www.computercraft.info/
function serialize( t ) -- converts any table to a string
  local tTracking = {}
  return serializeImpl( t, tTracking )
end

-- This function converts a serialized table back into a table.
-- It was written by dan200 for ComputerCraft (a Minecraft mod)
-- http://www.computercraft.info/
function unserialize( s ) -- converts a string to a table
  local func, e = loadstring( "return "..s, "serialize" )
  if not func then
    return s
  else
    --setfenv( func, {} )
    return func()
  end
end

-- Set up global variables.
function Initialize()

  -- Version info.
  version = 'Cache.lua 1.0.1'

  -- Tracks whether flush() was run.
  -- This allows us to run it many times without worrrying about
  --  clearing the cache more than once.
  wasRun = false

  -- This gets the location of the cache from <skinname>.ini.
  -- Because the cache is in @Resouces, you need to make sure that
  --  each skin using this has a unique value.
  cachePath = SKIN:ReplaceVariables('#@#'..SKIN:GetVariable('cachePath', nil))

  -- We check to ensure that cachePath exists.
  -- If not, we write a message to the log.
  checkOK(cachePath, 'The cachePath skin variable is not set!')

  -- These lines are specific to this implementation.
  -- They get two measures which we use later to determine the values to cache.
  measurePing1 = SKIN:GetMeasure('measurePing1')
  measurePing2 = SKIN:GetMeasure('measurePing2')

  -- The value to write to the cache when creating it.
  -- This string needs to contain a serialized table with all cache values in it.
  -- Otherwise, it's easy to end up with 'arithmetic on nil' errors and such.
  defaultValue = '{["pingMax"]=1,}'

  -- Write that we've completed Initialize() to the log.
  print(version..': Initialization complete.')

  -- Print where the cache should be located, or, if it isn't defined,
  --  the string 'Unknown!'.
  print('The cache is located at: '..(cachePath or 'Unknown!'))

end

-- This function reads data from the cache and returns it as a table.
function updateCache()
  -- Ensure the cache exists befoe we access it.
  checkOK(cachePath, 'The cachePath variable is not set!')

  -- Open the cache in binary-read mode and save the handle to 'cacheFile'.
  local cacheFile, err = io.open(cachePath, 'rb')

  if not cacheFile then -- If not cacheFile then the cache probably doesn't exist.
    -- We log a message stating that we're creating a new cache.
    print("The cache doesn't appear to exist.")
    print('Tring to create the cache...')

    -- This runs the echo command, and stores the output to the cache file.
    -- The '>' redirect will create a file if none exists, so we use that to create the cache.
    -- This line is responsible for the brief appearance of a black window.
    -- It should only appear once. If it keeps showing up, try running Rainmeter as an
    --  administrator.
    os.execute('@echo '..defaultValue..' >'..cachePath)

    -- Try to open the cache file again.
    cacheFile, err = io.open(cachePath, 'rb')
  end

  if not cacheFile then -- If not cacheFile then we have a problem, since we made sure it exists earlier.
    -- We log the fact that we couldn't access the cache,
    --  along with any errors provided by io.open.
    printError('A problem occurred during cache access:')
    printError(err)
  end
  
  -- This line retrieves the data in the cache, unserializes it,
  --  and stores the resulting table in 'cache'.
  local cache = unserialize(cacheFile:read('*a'))
  
  -- Always close file handles!
  -- You can lock up files if you aren't careful.
  cacheFile:close()

  -- Pass the table we got to the function that asked for it.
  return cache
end

-- This function writes a value to the cache, overwriting everything
--  that's already there.
function writeCache(value)
  -- Make sure the cache exists before we access it.
  checkOK(cachePath, 'The cachePath variable is not set!')

  -- Open the cache in binary-write mode and save the handle to 'cacheFile'.
  -- io.open returns nil and an error message if it fails, so we save both outputs.
  local cacheFile, err = io.open(cachePath, 'wb')

  -- Throw any errors passed by io.open if not cacheFile.
  checkOK(cacheFile, err)

  -- Serializes 'value', and writes the result to cacheFile's buffer.
  cacheFile:write(serialize(value))

  -- Writes the contents of cacheFile's buffer to disk.
  -- This is not the flush() function we define later on.
  cacheFile:flush()

  -- Always close file handles!
  -- You can lock up files if you aren't careful.
  cacheFile:close()
end

-- Called when [measureTimeAsInt] is less than 300000.
-- This function writes the defaults to all the values in the cache.
-- In this example, we don't want to flush ALL the cache data, so we
--  explicitly flush pingMax.
-- Any other cached data would be preserved.
function flush()
  -- Check whether this function was run before.
  -- If it wasn't, run it.
  -- This allows us to call the function many times, but only let it work once.
  if not wasRun then
    -- Get the current values in the cache.
    local cache = updateCache()

    -- Overwrite all the ones we want to be empty on boot.
    cache.pingMax = 1

    -- Write the new values to the cache.
    writeCache(cache)

    -- Set wasRun to true so we know that this was run.
    -- We run this at the end to prevent accidentally tricking the skin into
    --  thinking we succeeded even if there's an error.
    wasRun = true
  end
end

-- This function gets data from the cache and stores it in skin variables
--  so it can be accessed by meters, etc
-- We use skin variables so we can return multiple values at once.
-- We use 'return' to pass the error status.
function Update() -- will cache the highest recorded values of cache.up and cace.down
  -- Read data from the cache and keep it in 'cache'
  local cache = updateCache()

  -- This line is specific to this implentation
  -- It calculates the highest ping time encountered by comparing the values of
  --  the cache, measurePing1, and measurePing2
  -- We then store the highest of those in pingMax
  cache.pingMax = math.max(cache.pingMax, measurePing1:GetValue(), measurePing2:GetValue())

  -- Write the new values to the cache
  writeCache(cache)

  -- Set the skin variables
  SKIN:Bang('!SetVariable', 'PingMax', tostring(cache.pingMax))

  -- Pass the error status to [measureCacheScript] so the skin can do error checks
  return isOK
end
You do not have the required permissions to view the files attached to this post.
Last edited by skwerlman on May 18th, 2014, 6:10 am, edited 1 time in total.
skwerlman
Posts: 6
Joined: May 15th, 2014, 6:32 pm

Re: Persistent data

Post by skwerlman »

Caching Example 1.0.1

Changelog:
Script:
fixed several typos
some more logging
now only gets the cachePath skin var once, rather than on each read/write
(The code was done, but I forgot to implement it :P)
improved script documentation a little
added version number to script (hehe, i forgot to do that before...)

Skin:
Fixed some typos
accidentally put Update=1000a instead of Update=1000

General:
forgot to mention that the cache is cleared when the computer is turned on (or the skin is refreshed within five minutes of boot).
You can disable this by removing all the measures marked 'ClearCacheOnReboot'.