2015-05-26 20:32:29 +02:00
# encoding: utf-8
2015-07-30 08:21:20 +02:00
# ruby: 1.9
2015-05-26 20:32:29 +02:00
class Telegram
attr_accessor :direction , :cast , :transmission , :node , :object , :data
# transmission types
RESERVED = 0
QUERY = 1
ANSWER = 2
SEND = 3
# object types
OBJECTS = [ ]
OBJECTS [ 0 ] = " device type "
OBJECTS [ 1 ] = " serial no. "
OBJECTS [ 2 ] = " nominal voltage (V) "
OBJECTS [ 3 ] = " nominal current (A) "
OBJECTS [ 4 ] = " nominal power (W) "
OBJECTS [ 6 ] = " article no. "
OBJECTS [ 8 ] = " manufacturer "
OBJECTS [ 9 ] = " software version "
OBJECTS [ 19 ] = " device class "
OBJECTS [ 38 ] = " over voltage protection threshold (%V) "
OBJECTS [ 39 ] = " over current protection threshold (%A) "
OBJECTS [ 50 ] = " set voltage (%V) "
OBJECTS [ 51 ] = " set current (%I) "
OBJECTS [ 54 ] = " power supply control "
OBJECTS [ 71 ] = " status and actual values "
OBJECTS [ 72 ] = " status and set values "
2015-08-24 16:10:46 +02:00
OBJECTS [ 149 ] = " switch to bootlaoder? "
OBJECTS [ 150 ] = " unlock code 1? "
OBJECTS [ 151 ] = " unlock code 2? "
OBJECTS [ 152 ] = " write/check memory? "
2015-05-26 20:32:29 +02:00
OBJECTS [ 255 ] = " error "
# length of the object type
LENGTHS = [ ]
LENGTHS [ 0 ] = 16
LENGTHS [ 1 ] = 16
LENGTHS [ 2 ] = 4
LENGTHS [ 3 ] = 4
LENGTHS [ 4 ] = 4
LENGTHS [ 6 ] = 16
LENGTHS [ 8 ] = 16
LENGTHS [ 9 ] = 16
LENGTHS [ 19 ] = 2
LENGTHS [ 38 ] = 2
LENGTHS [ 39 ] = 2
LENGTHS [ 50 ] = 2
LENGTHS [ 51 ] = 2
LENGTHS [ 54 ] = 2
LENGTHS [ 71 ] = 6
LENGTHS [ 72 ] = 6
2015-08-24 16:10:46 +02:00
LENGTHS [ 149 ] = 4
LENGTHS [ 150 ] = 4
LENGTHS [ 151 ] = 4
LENGTHS [ 152 ] = 8
2015-05-26 20:32:29 +02:00
LENGTHS [ 255 ] = 1
# possibles errors
ERRORS = [ ]
ERRORS [ 0 ] = " no error "
ERRORS [ 3 ] = " checksum incorrect "
ERRORS [ 4 ] = " start delimiter incorrect "
ERRORS [ 5 ] = " wrong address for output "
ERRORS [ 7 ] = " object not defined "
ERRORS [ 8 ] = " object length incorrect "
ERRORS [ 9 ] = " no access permission "
ERRORS [ 15 ] = " device in lock state "
ERRORS [ 48 ] = " upper limit exceeded "
ERRORS [ 49 ] = " lower limit exceeded "
# create a query or send telegram for this object
# query if data is nil, else send data
2015-07-30 09:49:22 +02:00
# direction: true = control unit to device, false = device to control unit
def initialize ( object , data = nil , direction = true )
2015-05-26 20:32:29 +02:00
# telegram direction: true = control unit to device, false = device to control unit
2015-07-30 09:49:22 +02:00
@direction = direction
2015-05-26 20:32:29 +02:00
# cast type: true = query, false = answer
2015-07-30 09:49:22 +02:00
@cast = direction
2015-05-26 20:32:29 +02:00
# device node
@node = 0
# set object
@object = object
# verify data
2015-07-30 09:49:22 +02:00
if direction then
if data == nil or data . empty? then
@transmission = QUERY
@data = [ ]
else
raise " wrong data length " if LENGTHS [ @object ] and data . length! = LENGTHS [ @object ]
@transmission = SEND
@data = data
end
2015-05-26 20:32:29 +02:00
else
2015-07-30 09:49:22 +02:00
raise " wrong data length " if data . empty? or ( LENGTHS [ @object ] and data . length > LENGTHS [ @object ] )
@transmission = ANSWER
2015-05-26 20:32:29 +02:00
@data = data
end
end
# create a Telegram from the raw telegram data
def Telegram . parse ( telegram )
# check there are at least 5 bytes (minimum message size)
return nil if telegram == nil
return nil if telegram . length < 5
2015-07-30 08:21:20 +02:00
bytes = telegram . bytes . to_a # get bytes
2015-05-26 20:32:29 +02:00
to_return = new ( bytes [ 2 ] ) # new Telegram
# parse start delimiter (SD)
length = bytes [ 0 ] & 0x0f # get length
to_return . direction = ( bytes [ 0 ] & 0x10 != 0 )
to_return . cast = ( bytes [ 0 ] & 0x20 != 0 )
to_return . transmission = ( ( bytes [ 0 ] >> 6 ) & 0x03 )
# parse device node (DN)
to_return . node = bytes [ 1 ]
# parse object (OBJ)
to_return . object = bytes [ 2 ]
# parse data field
to_return . data = bytes [ 3 .. - 3 ]
# parse checksum (CS)
checksum = ( bytes [ - 2 ] << 8 ) + bytes [ - 1 ]
2015-07-30 08:54:01 +02:00
bytes [ 0 .. - 3 ] . each { | b | checksum -= b }
2015-05-26 20:32:29 +02:00
# run some checks
raise " wrong length. expected #{ length } , got #{ to_return . data . length - 1 } " if ( to_return . transmission == SEND or to_return . transmission == ANSWER ) and length != to_return . data . length - 1
2015-07-30 08:54:01 +02:00
raise " wrong checksum. off by #{ checksum } " if checksum != 0
2015-05-26 20:32:29 +02:00
return to_return
end
# is the telegram from the control unit to the device
def to_device?
return @direction
end
# is the telegram from the device to the control unit
def to_control?
return ! @direction
end
# is the telegram a query
def query?
return @cast
end
# is the telegram an answer
def answer?
return ! @cast
end
# pack all except checksum
def pack_data
# make start delimiter
raise " too much data " if data . length - 1 > 0xf
length = nil
if @transmission == ANSWER or @transmission == RESERVED then
raise " wrong data field length. expected #{ LENGTHS [ @object ] } , got #{ data . length } " if LENGTHS [ @object ] and data . length > LENGTHS [ @object ]
end
if @data and ! @data . empty? then
start = data . length - 1
elsif LENGTHS [ @object ] then
start = LENGTHS [ @object ] - 1
else
start = 0x0f
end
start += 1 << 4 if @direction
start += 1 << 5 if @cast
start += @transmission << 6
# add rest
return [ start , @node , @object ] + @data
end
# pack telegram as string
def pack
# calculate checksum
data = pack_data
data += [ checksum >> 8 , checksum & 0xff ]
return data . pack ( " C* " )
end
# calculate checksum
def checksum
# calculate checksum
data = pack_data
cs = 0
data . each { | b | cs += b }
return cs
end
# check packet
def check_direction
raise " wrong direction " if ( @direction and ! @cast ) or ( @cast and ! ( @transmission == QUERY or @transmission == SEND ) ) or ( ! @direction and @cast ) or ( ! @cast and ! ( @transmission == ANSWER or @transmission == RESERVED ) )
end
def to_s
str = @direction ? " < " : " > "
if OBJECTS [ @object ] then
if @object == 0 and @data . length == 2 and @data [ 0 ] == 0xFF then # bug in the firmware. error should use object FF but uses object 00 and first byte is FF. second byte is error"
str += " error "
else
str += " " + OBJECTS [ @object ]
end
else
str += " #{ @object } "
end
if @data and ! @data . empty? then
str += " : "
case @object
when 0 # string or error (that's a bug)
if @data . length == 2 and @data [ 0 ] == 0xFF then # error
str += ( ERRORS [ @data [ 1 ] ] or " unknown " )
else # string
data = @data . pack ( " C* " )
str_end = data . index ( " \0 " )
str_end = data . length unless str_end
str += data [ 0 , str_end ]
end
when 1 , 6 , 8 , 9 # strings
data = @data . pack ( " C* " )
str_end = data . index ( " \0 " )
str_end = data . length unless str_end
str += data [ 0 , str_end ]
when 2 , 3 , 4 # float
str += @data . pack ( " C* " ) . unpack ( " g " ) [ 0 ] . to_s
when 19 # id
str += if @data == [ 0x00 , 0x10 ] then
" PS 2000 B Single "
elsif @data == [ 0x00 , 0x18 ] then
" PS 2000 B Triple "
else
" unknown "
end
when 38 , 39 , 50 , 51 # percentage
str += ( @data . pack ( " C* " ) . unpack ( " n " ) [ 0 ] / 256 . 0 ) . round ( 3 ) . to_s
when 54 # changes
changes = [ ]
changes << ( ( @data [ 1 ] & 0x01 ) == 0 ? " output off " : " output on " )
changes [ - 1 ] += " (changed) " if ( @data [ 0 ] & 0x01 ) != 0
changes << ( ( @data [ 1 ] & 0x0A ) == 0 ? nil : " acknowledge alarm " )
changes [ - 1 ] += " (changed) " if ( @data [ 0 ] & 0x0A ) != 0
changes << ( ( @data [ 1 ] & 0x10 ) == 0 ? " manual control " : " remote control " )
changes [ - 1 ] += " (changed) " if ( @data [ 0 ] & 0x10 ) != 0
changes << " tracking on " if @data [ 1 ] & 0xF0 == 0xF0
changes << " tracking off " if @data [ 1 ] & 0xF0 == 0xF0
changes [ - 1 ] += " (changed) " if ( @data [ 0 ] & 0xF0 ) != 0
str += changes . compact * " , "
when 71 , 72 # status + values
status = [ ]
status << if @data [ 0 ] & 0x03 == 0x00 then
" free access "
elsif @data [ 0 ] & 0x03 == 0x01 then
" free access "
else
" unknown access "
end
status << ( ( @data [ 1 ] & ( 1 << 1 ) ) == 0 ? " output off " : " output on " )
status << if @data [ 1 ] & 0x06 == 0x00 then
" constant voltage "
elsif @data [ 1 ] & 0x06 == 0x04 then
" constant current "
else
" unknown controller state "
end
status << ( ( @data [ 1 ] & ( 1 << 3 ) ) == 0 ? " tracking off " : " tracking on " )
status << ( ( @data [ 1 ] & ( 1 << 4 ) ) == 0 ? " over-voltage protection off " : " over-voltage protection on " )
status << ( ( @data [ 1 ] & ( 1 << 5 ) ) == 0 ? " over-current protection off " : " over-current protection on " )
status << ( ( @data [ 1 ] & ( 1 << 6 ) ) == 0 ? " over-power protection off " : " over-power protection on " )
status << ( ( @data [ 1 ] & ( 1 << 7 ) ) == 0 ? " over-temperature protection off " : " over-temperature protection on " )
str += status . compact * " , "
str += " , voltage %: " + ( @data [ 2 , 2 ] . pack ( " C* " ) . unpack ( " n " ) [ 0 ] / 256 . 0 ) . round ( 3 ) . to_s
2015-08-24 16:10:46 +02:00
str += " , current01 %: " + ( @data [ 4 , 2 ] . pack ( " C* " ) . unpack ( " n " ) [ 0 ] / 256 . 0 ) . round ( 3 ) . to_s
when 152 # write memory
case @data [ 0 ]
when 0x33
str += " select section #{ @data [ 1 ] } "
when 0x34
str += " flush? "
when 0x30
str += " end of write? "
else
str += " page #{ @data [ 0 ] } : "
str += @data [ 1 .. - 1 ] . collect { | b | sprintf ( " %02X " , b ) } . join
end
2015-05-26 20:32:29 +02:00
when 255
str += ( ERRORS [ @data [ 0 ] ] or " unknown " )
else
str += @data . collect { | b | sprintf ( " %02X " , b ) } . join if @data and ! @data . empty?
end
end
return str
end
end