Skip to content

Instantly share code, notes, and snippets.

@alakra
Created July 26, 2012 19:23
Show Gist options
  • Save alakra/3183987 to your computer and use it in GitHub Desktop.
Save alakra/3183987 to your computer and use it in GitHub Desktop.
S-Expression compiler for Ladder Logic (version 2)
# luby2.rb --
#
# This file translates S-Expressions as a mark-up language into ladder
# logic which can be used by the Ladder Logic Editor by Sylva Control Systems.
#
# Copyright (c) 2008 RODI Systems, Inc.
#
# See the file "LICENSE" for more information on usage and redistribution of
# this file and for a DISCLAIMER OF ALL WARRANTIES.
require 'yaml'
require 'pp'
class Luby
def initialize(input, output, reserved, template, title)
@template = YAML.load_file(template)
@cputype = @template['name']
@reserved = File.exists?(reserved) ? YAML.load_file(reserved) : nil
@source = File.read(input)
@output = output
@rendered = nil
@title = title
@dbfile = File.join(File.dirname(@output),"dbfile.yml")
@database = DB.new(@template, @reserved)
end
def compile
output = "C8051F124\n"
stuff = analyze tokenize
# Render Rungs
stuff[:rungs].each_with_index do |rung, i|
rung[2] = "\n-------\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"*10 if rung[2].nil?
output << "Untitled\n#{i+1}\n#{rung[0]}\n#{rung[2]}#{rung[1]}"
end
@rendered = output
@rendered << "*****END OF LADDER*****\n"
# Render Math
128.times do |i|
@rendered << stuff[:maths][i] unless stuff[:maths][i].nil?
@rendered << "\n*****END OF BASIC#{i+1}*****\n"
end
# Render PID
@rendered << "0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n4095\n0\n0\n0\n0\n1\n0\n"*8 + "*****END OF PID*****\n"
# Render Unused AL350 Screen
@rendered << " USER COMPANY NAME\n--------------------\n USER PROGRAM INFO\n USER PROGRAM INFO\n"
# Render Unused Messages
@rendered << "\n"*320 + "*****END OF MESSAGES*****\n"
# Rendered Bit Table Description
@rendered << "\n"*2048 + "*****END OF BIT TABLE*****\n"
# Rendered Word Table Description
@rendered << "\n"*4096 + "*****END OF WORD TABLE*****\n"
# Rendered Unused Text Screens
@rendered << "\n"*128 + "*****END OF TEXT SCREENS*****\n"
# Rendered Unused Setpoints
@rendered << "\n"*1152 + "*****END OF SETPOINTS*****\n"
# Rendered MODBUS type
@rendered << "SLAVE\n"
# Rendered MODBUS Settings
@rendered << "\n"*224 + "*****END OF MODBUS*****\n"
# Rendered Unused Calibration Screens
@rendered << "\n"*576 + "*****END OF CALIBRATION*****\n"
# Rendered Unused I2C
@rendered << "\n"*112 + "*****END OF I2C*****\n\n\n\n\n\n\n\n\n"
end
def save
f = File.open(@output, "w")
f.write(@rendered)
f.close
end
def save_db
f = File.open(@dbfile,"w")
f.write(@database.to_yaml)
f.close
end
def get_db
@database
end
private
def tokenize
print "Tokenizing..."
pattern = /#.+\n??|\n|\(|\)|'.+'|[a-zA-Z0-9_\-]+!?|'|\[\]/
counter = 0
lineno = 0
# Tokenize based on pattern and convert to symbols
tokens = []
@source.scan(pattern) {|token| tokens << token.to_sym }
# Catch any errors parantheses errors
tokens.each do |match|
counter += 1 if match == :"("
counter -= 1 if match == :")"
lineno += 1 if match == :"\n"
if ((match == :rung || match == :basic) && counter != 1)
print "Error: Parantheses stack does not equal zero! Check line number #{lineno} and make sure that you have used the correct number of parantheses.\n"
exit
end
end
# Delete comments from token list
tokens.delete_if {|x| x.to_s =~ /^#.+/ }
print "Done.\n"
return tokens
end
def analyze(tokens)
print "Analyzing..."
# Create stacks
rungs = Stack.new
maths = Hash.new
temp = Stack.new
#Analyze Luby
tokens.each do |token|
unless token == :"\n"
construct = []
if token == :")"
construct << temp.pop until temp.current == :"("
temp.pop # one more to get rid of the last paranthesis
# Fill correct stack
if construct.last == :rung
rungs.push(evaluate(construct))
elsif construct.last == :basic
maths.update(construct.first[:mt] => evaluate(construct))
else
temp.push(evaluate(construct))
end
else
temp.push(token)
end
end
end
print "Done.\n"
return {:rungs => rungs, :maths => maths}
end
def evaluate(group)
function = group.pop
parameters = group
case function
when :basic
output = ""
until group.empty?
item = group.pop
unless (item.to_s[0..0].to_i > 0) || (item[0..0] == "0") || item.is_a?(Hash)
item = "REM " + item.to_s
end
output << item.to_s + "\n" unless item.is_a? Hash
end
when :comment
output = group.pop.to_s.gsub("'","")
output = output.sub(/%(\w+)/) do |match|
@database.lookup(:subroutines, "#{$1}".to_sym)
end
when :counter
countername = group.pop
table = group.pop
page = group.length > 1 ? group.pop : nil
variable = group.pop
counter = @database.lookup(table, variable, page)
output = [:"C:#{counter}\n", :"-(CTU)-\n", :"\n\n"]
when :ctdone
variable = group.pop
counter = @database.lookup(:counters, variable)
bit = @database.info(:counterdone)[:range].first + counter
output = [:"B:#{bit}\n", :"--] [--\n", :"#{variable}\n\n"]
when :dlx
output = get_timer(group, :"-(DLX)-\n")
when :dly
output = get_timer(group, :"-(DLY)-\n")
when :eol
output = [:"\n", :"-(EOL)-\n", :"\n\n"]
when :filter
filter = group.pop
table = group.pop
variable = group.pop
source = @database.lookup(table, variable)
output = [:"F:#{filter}\n", :"-(DSP)-\n", :"\n\n",
:"W:#{source}\n", :"-(SRV)-\n", :"\n\n"]
when :flipflop
table = group.pop
variable = group.pop
flipflop = @database.lookup(table, variable)
output = [:"B:#{flipflop}\n", :"-(FFP)-\n", :"#{variable}\n\n"]
when :input
#bundle = Stack.new # for branches
columnlength = 0
output = group.reverse.collect do |column|
if column.length > 4
firstcol = aggregate_input_cols(column[0..2])
seccol = aggregate_input_cols(column[3..-1])
columnlength += 2
columnoutput = firstcol + seccol
else
columnlength += 1
columnoutput = aggregate_input_cols(column)
end
end.join
(10-columnlength).times { output << "\n-------\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" }
when :line
output = group.pop.to_s.gsub("'","")
# Modify bit values
output = output.gsub(/(otl|otu|xih|xil)\s([a-zA-Z0-9_\[\]]+)/i) do |match|
command = "#{$1}".upcase
variable = "#{$2}"
group = variable.scan(/[a-zA-Z0-9_]+/)
# get correct value
unless group[0].to_i > 0
if group.length > 2
value = @database.lookup(group[0].to_sym, group[2].to_sym, group[1])
else
value = @database.lookup(group[0].to_sym, group[1].to_sym)
end
end
# get timers
if group[0] == "onetimers"
result = "#{command} #{sprintf("%04d",@database.info(:onetimersdone)[:range].first + (value - 65))}"
elsif group[0] == "64timers"
result = "#{command} #{sprintf("%04d",@database.info(:"64timersdetails")[:range].first + value)}"
elsif group[0] == "oneshots"
result = "#{command} #{sprintf("%04d",@database.info(:oneshotsdetails)[:range].first + value)}"
else
result = (value.nil?) ? "#{command} #{sprintf("%04d", variable.to_i)}" : "#{command} #{sprintf("%04d",value)}"
end
end
# Modify word values
output = output.gsub(/word\((.*?)\)/) do |match|
if match.include?("[")
tokens = match.scan(/[a-zA-Z0-9_]+/)
timer_detail = match.scan(/\[pre\]|\[acc\]/)
if timer_detail.length > 0
timer = @database.lookup(tokens[1].to_sym, tokens[2].to_sym)
if tokens[1].to_sym == :onetimers
detail = @database.info(:onetimersdetails)[:range].first + (timer - 65)*2
else
detail = @database.info(:"64timersdetails")[:range].first + timer*2
end
detail += 1 if timer_detail.last == "[pre]"
result = "word(#{detail})"
else
tokens = match.scan(/\w+\[\w+\]\[\w+\]|\w+\[\w+\]/)
tokens.each do |token|
q = token.scan(/\w+/)
if q.length > 2
match = match.gsub(token, @database.lookup(q[0].to_sym, q[2].to_sym, q[1]).to_s)
else
match = match.gsub(token, @database.lookup(q[0].to_sym, q[1].to_sym).to_s)
end
end
result = match
end
else
result = "#{match}"
end
end
when :ltdone
table = group.pop
variable = group.pop
timer = @database.lookup(table, variable)
if table == :onetimers
bit = @database.info(:onetimersdone)[:range].first + (timer - 65)
else
bit = @database.info(:"64timersdone")[:range].first + timer
end
output = [:"B:#{bit}\n", :"--]/[--\n", ":#{variable}\n\n"]
when :math
variable = group.pop
math = @database.lookup(:basic, variable)
output = [:"P:#{math}\n", :"-(BAS)-\n", :"#{variable}\n\n"]
when :mathtracker
output = {:mt => @database.lookup(:basic, group.pop)}
when :ost
name = group.pop
variable = group.pop
oneshot = @database.lookup(name, variable)
output = [:"O:#{oneshot}\n", :"-[OST]-\n", :"#{variable}\n\n"]
when :ote
output = bit_set(group, :"-(OTE)-\n")
when :otl
output = bit_set(group, :"-(OTL)-\n")
when :otu
output = bit_set(group, :"-(OTU)-\n")
when :output
alloutput = group.reverse.flatten.join
length = 0
alloutput.each_byte {|c| length = length.next if c == 10 }
if length <= 4
precolumn = "\n-------\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
else
precolumn = "\n---+---\n"
branches = (length / 4) - 1
branches.times { precolumn << " |\n |\n |\n x---\n" }
newlines = (22-(branches*4)).times { precolumn << "\n"}
end
output = precolumn + alloutput + "\n"*(24-length)
when :ret
output = [:"\n", :"-(<--)-\n", :"\n\n"]
when :ros
table = group.pop
variable = group.pop
oneshot = @database.lookup(table, variable)
output = [:"O:#{oneshot}\n", :"-(ROS)-\n", :"#{variable}\n\n"]
when :rstcount
variable = group.pop
counter = @database.lookup(:counters , variable)
output = [:"C:#{counter}\n", :"-(CTR)-\n", :"#{variable}\n\n"]
when :rstcdone
variable = group.pop
counter = @database.lookup(:counters, variable)
bit = @database.info(:counterdone)[:range].first + counter
output = [:"B:#{bit}\n", :"-(OTU)-\n", :"#{variable}\n\n"]
when :rsttdone
timer_type = group.pop
variable = group.pop
timer = @database.lookup(timer_type, variable)
if timer_type == :onetimers
bit = @database.info(:onetimersdone)[:range].first + (timer - 65)
else
bit = @database.info(:"64timersdone")[:range].first + timer
end
output = [:"B:#{bit}\n", :"-(OTU)-\n", :"#{variable}\n\n"]
when :rsttimer
if (group.length < 2)
timer = group.pop
else
table = group.pop
variable = group.pop
timer = @database.lookup(table, variable)
end
output = [:"T:#{timer}\n", :"-(RST)-\n", :"#{variable}\n\n"]
when :rung
output = group
when :settdone
timer_type = group.pop
variable = group.pop
timer = @database.lookup(timer_type, variable)
if timer_type == :onetimers
bit = @database.info(:onetimersdone)[:range].first + (timer - 65)
else
bit = @database.info(:"64timersdone")[:range].first + timer
end
output = [:"B:#{bit}\n", :"-(OTL)-\n", :"#{variable}\n\n"]
when :sub
variable = group.pop
subroutine = @database.lookup(:subroutines, variable)
output = [:"S:#{subroutine}\n", :"-(->>)-\n", :"#{variable}\n\n"]
when :timerdone
timer_type = group.pop
variable = group.pop
timer = @database.lookup(timer_type, variable)
if timer_type == :onetimersdone
bit = @database.info(:onetimersdone)[:range].first + (65 - timer)
else
bit = @database.info(:"64timersdone")[:range].first + timer
end
output = [:"B:#{bit}\n", :"--] [--\n", :"#{variable}\n\n"]
when :wequal
output = word_compare(group, :"--[=]--\n")
when :"wequal!"
output = word_compare(group, :"-[< >]-\n")
when :wcopy
if (group.last.to_s.to_i > 0 || group.last == :"0")
number = group.pop
table = group.pop
page = (group.length > 1) ? group.pop : nil
variable = group.pop
destination = @database.lookup(table, variable, page)
output = [:"K:#{number}\n", :"-(CPY)-\n", :"#{number}\n\n",
:"W:#{destination}\n", :"-(DST)-\n", :"#{variable}\n\n"]
else
source_table = group.pop
page_one = (group.last.to_s.to_i > 0 || group.last == :"0") ? group.pop : nil
var_one = group.pop
dest_table = group.pop
page_two = (group.last.to_s.to_i > 0 || group.last == :"0") ? group.pop : nil
var_two = group.pop
source = @database.lookup(source_table, var_one, page_one)
destination = @database.lookup(dest_table, var_two, page_two)
output = [:"W:#{source}\n", :"-(CPY)-\n", :"#{var_one}\n\n",
:"W:#{destination}\n", :"-(DST)-\n", :"#{var_two}\n\n"]
end
when :wgt
output = word_compare(group, :"--[>]--\n")
when :wgtequal
output = word_compare(group, :"-[> =]-\n")
when :wlt
output = word_compare(group, :"--[<]--\n")
when :wltequal
output = word_compare(group, :"-[< =]-\n")
when :xih
output = bit_compare(group, :"--] [--\n")
when :xil
output = bit_compare(group, :"--]/[--\n")
end
return output
end
def aggregate_input_cols(colgroup)
length = 0
col = colgroup.join
col.each_byte {|c| length = length.next if c == 10 }
(24-length).times { col << "\n" }
return col
end
private
def word_compare(set, type)
if (set.first.to_s.to_i != 0 || set.first == :"0")
table = set.pop
page = (set.length > 2) ? set.pop : nil
variable = set.pop
number = set.pop
constant = number.to_s.to_i < 0 ? :"K#{number}\n" : :"K:#{number}\n"
comparison = @database.lookup(table, variable, page)
output = [:"W:#{comparison}\n", :"--[W]--\n", :"#{variable}\n\n",
constant, type, :"#{number}\n\n"]
else
source_table = set.pop
page_one = (set.last.to_s.to_i > 0 || set.last == :"0") ? set.pop : nil
var_one = set.pop
dest_table = set.pop
page_two = (set.last.to_s.to_i > 0 || set.last == :"0") ? set.pop : nil
var_two = set.pop
source = @database.lookup(source_table, var_one, page_one)
destination = @database.lookup(dest_table, var_two, page_two)
output = [:"W:#{source}\n", :"--[W]--\n", :"#{var_one}\n\n",
:"W:#{destination}\n", type, :"#{var_two}\n\n"]
end
return output
end
def bit_compare(set, type)
name = set.pop
variable = set.pop
if (name.to_s.to_i > 0 || name == :"0")
bit = name.to_s
else
bit = @database.lookup(name, variable)
end
return [:"B:#{bit}\n", type, variable, :"\n\n"]
end
def bit_set(set, type)
name = set.pop
if (name.to_s.to_i > 0 || name == :"0")
bit = name.to_s
output = [:"B:#{bit}\n", type, :"#{bit}\n\n"]
else
variable = set.pop
bit = @database.lookup(name, variable)
output = [:"B:#{bit}\n", type, :"#{variable}\n\n"]
end
return output
end
def get_timer(set, type)
if set.last.to_s.to_i > 0 || set.last == :"0"
timer = set.pop
timer_var = timer
output = [:"T:#{timer}\n", type, :"#{timer_var}\n\n"]
else
timer_table = set.pop
timer_var = set.pop
timer = (timer_var.to_s.to_i > 0 || timer_var == :"0") ? timer_var : @database.lookup(timer_table, timer_var)
output = [:"T:#{timer}\n", type, :"#{timer_var}\n\n"]
end
if set.length > 1
word_table = set.pop
page = set.length > 1 ? set.pop : nil
variable = set.pop
word = (word_table.to_s.to_i > 0 || word_table == :"0") ? word_table : @database.lookup(word_table, variable, page)
output << [:"V:#{word}\n", :"-(SRV)-\n", :"#{variable}\n\n"]
else
constant = set.pop
output << [:"K:#{constant}\n", :"-(SRK)-\n", :"\n\n"]
end
return output
end
protected
class Stack
include Enumerable
def initialize
@store = []
end
def push(x)
@store.push x
end
def pop
@store.pop
end
def current
@store.last
end
def all
@store
end
def each(&block)
@store.each {|x| block.call(x) }
end
end
class DB
def initialize(template, reserved)
@template = template
@reserved = reserved
@db = {}
load_template
if @reserved.nil?
print "Not using reservations.\n"
else
load_reserved
end
end
def info(tablename)
return @db[tablename] if @db[tablename].is_a? Array
@db[tablename].info
end
def lookup(tablename, variable, page = nil)
if page.nil?
if @db[tablename].is_a? Array
table = @db[tablename].find {|container| container.member?(variable) }
if table.nil?
arb_table = @db[tablename].find {|container| container.full? }
arb_table.get(variable)
else
table.get(variable)
end
else
@db[tablename].get(variable)
end
else
@db[tablename][page.to_s.to_i].get(variable)
end
end
private
def load_template
@template.each do |name, value|
@db.update(name.to_sym => Container.new(name, value)) if value.is_a? Range
if value.is_a? Array
pages_of_containers = value.collect {|range| Container.new(name, range)}
@db.update(name.to_sym => pages_of_containers)
end
end
end
def load_reserved
@reserved.each do |datatype, values|
table = @db[datatype.to_sym]
values.each do |name, address|
if table.is_a? Container
table.set(name.to_sym, :address => address, :type => :manual)
else
table.each do |container|
if container.inrange?(address)
container.set(name.to_sym, :address => address, :type => :manual)
end
end
end
end
end
end
class Container
attr_reader :name, :current
def initialize(name, range)
@name = name
@range = range
@first = range.first
@current = range.first
@active = nil
@last = range.last
@store = {}
end
def info
{ :name => @name,
:range => @range,
:current => @current,
:store => @store }
end
def set(newname, options = {})
options[:type] ||= :auto
options[:address] ||= @current
@active = newname
# Return immediately if not in range
return nil if !inrange?(options[:address])
# Return immediately if store is full
return nil if full? #should thrown
if @store.has_value? options[:address]
inc
set(newname)
else
@store.update(newname => options[:address])
inc if options[:type] == :auto
end
return @store[newname]
end
def get(curname)
set(curname) if @store[curname].nil?
return @store[curname]
end
def member?(name)
@store[name].nil? ? false : true
end
def full?
@current > @last ? true : false
end
def inrange?(number)
@range.member?(number)
end
def is_of_the(name_to_check)
name_to_check == @name ? true : false
end
private
# throw exception when last value reached
def inc
if @current > @last
raise StandardError, "#{active} on #{@name} at #{@current}"
else
@current = @current + 1
end
end
def dec
@current = @current <= @last ? @first : @current - 1
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment