From 2481abb8af4fc215e156cce70566dc53ef629522 Mon Sep 17 00:00:00 2001 From: Ferdi Franceschini Date: Wed, 15 Jan 2014 15:27:19 +1100 Subject: [PATCH] Scripts which generate a motor configuration file from CSV data files. --- site_ansto/instrument/util/genmotconf.tcl | 200 +++++++++++++++ .../instrument/util/genmotconf_procs.tcl | 231 ++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100755 site_ansto/instrument/util/genmotconf.tcl create mode 100755 site_ansto/instrument/util/genmotconf_procs.tcl diff --git a/site_ansto/instrument/util/genmotconf.tcl b/site_ansto/instrument/util/genmotconf.tcl new file mode 100755 index 00000000..f48cfeb8 --- /dev/null +++ b/site_ansto/instrument/util/genmotconf.tcl @@ -0,0 +1,200 @@ +#!/usr/bin/env tclsh +# @file Generate a motor configuration file from CSV files of name value pairs. +# +# Input: List of CSV files. +# Output files: +# generated_motor_configuration.tcl +# genmotconf_report.log +# genmotconf_errors.log: Lists missing attributes if a spec is incomplete. +# missing_attlist.csv +# +# TODO +# Optionally split configuration accross multiple files for cases where +# axes are swapped out, eg Eulerian cradles, sample-stick rotation. +# This could be done by supplying a file which contains lines as follows, +# CFG_NAME1,m1 m2 m3 +# CFG_NAME2,m4 m5 m6 +# Where CFG_NAMEn is the name of a config file and mn's are motor names. +# In this case generated_motor_configuration.tcl will define the header and asyncqueues. +# The motor_configuration.tcl file would then be hand-coded to fileeval the CFG_NAMEn files as required. +# +source [file dirname $argv0]/genmotconf_procs.tcl + +set ERRCNT 0 +set MOTCFG_CNT 0 +set FAILED_MOTCFG_CNT 0 +# MOT_ATTLIST: Attributes required to configure an axis without an encoder +# ENC_ATTLIST: Attributes required to describe an encoder. +# NOTE Encoder readings for the limit switch positions are required. +# If the encoder "home" reading is not supplied it is set equal to rev_enc_lim +# ENCMOT_ATTLIST: Attributes which describe an axis which has both a motor and encoder. +# REQ_ATTLIST: List of attributes required to generate a configuration for a motor object. +# SICS_CFG_ATTLIST: Extra attributes required to configure a motor object in SICS. +# ALL_ATTRIBUTES: List of all attributes recognised by this program. +# +# Attributes which are not in these lists are assumed to define the encoder +# readings for each position on an axis which has a set of meaningful positions +# such as apertures or multi-sample tables. +set MOT_ATTLIST [lsort {axis mc steps_per_x}] +set ENC_ATTLIST [lsort {absenchome cnts_per_x fwd_enc_lim rev_enc_lim}] +set ENCMOT_ATTLIST [lsort [concat $MOT_ATTLIST ENCMOT_ATTLIST]] +set SICS_CFG_ATTLIST [lsort {home fwd_lim rev_lim maxspeed maxaccel maxdecel part units}] +set REQ_ATTLIST [lsort [concat $MOT_ATTLIST $SICS_CFG_ATTLIST]] +set ALL_ATTRIBUTES [lsort [concat $ENCMOT_ATTLIST $SICS_CFG_ATTLIST]] + +set scriptname [file tail [file rootname $argv0]] +set file_list $argv + +set fhr [open ${scriptname}_report.log "w"] +set fhe [open ${scriptname}_errors.log "w"] + +# @brief Generate a default value for missing attributes. +# @param mot motor name +# @param att attribute name +# @return value for attribute or NOATT if no attribute should be generated. +proc gen_attval {mot att} { + switch $att { + "absenchome" { + if [info exists ::${mot}_encatts(rev_enc_lim)] { + return [set ::${mot}_encatts(rev_enc_lim)] + } else { + return "NOATT" + } + } + "maxspeed" {return 1} + "maxaccel" {return 1} + "maxdecel" {return 1} + "rev_lim" {return 0} + "home" {return 0} + "fwd_lim" { + if { [info exists ::${mot}_encatts(fwd_enc_lim)] && [info exists ::${mot}_encatts(cnts_per_x)] } { + set fwd_enc_lim_val [set ::${mot}_encatts(fwd_enc_lim)] + set cnts_per_x_val [set ::${mot}_encatts(cnts_per_x)] + + if [info exists ::${mot}_encatts(rev_enc_lim)] { + set rev_enc_lim_val [set ::${mot}_encatts(rev_enc_lim)] + } else { + set rev_enc_lim_val 0 + } + if [info exists ::${mot}_encatts(home)] { + set home_val [set ::${mot}_encatts(home)] + } else { + set home_val 0 + } + return [expr {($fwd_enc_lim_val - $rev_enc_lim_val) / $cnts_per_x_val + $home_val}] + } else { + return 1 + } + } + "part" {return "instrument"} + "units" {return "xxx"} + } +} + +################################################################################ +# Parse all files and generate ::mn(matt) and ::controllers(cn) +foreach f $file_list { + parse_file $f $fhr $fhe +} + +################################################################################ +# GENERATE MOTOR CONFIGURATION FILE +puts $fhr "GENERATE MOTOR CONFIGURATIONS" +puts $fhe "GENERATE MOTOR CONFIGURATIONS" +puts $fhe "Required attributes: $REQ_ATTLIST" + +set fhmc [open "generated_motor_configuration.tcl" "w"] +# Write configuration file header and make asyncqueues +mk_cfg_header $fhmc +puts $fhmc "" + +foreach mn [lsort [array names motor_attcnt]] { + set mot_attlist [lsort [array names $mn]] + set mot_encattlist [lsort [array names ${mn}_encatts]] + set num_encatts [llength $mot_encattlist] + set missing_enc_atts [setdiff $ENC_ATTLIST $mot_encattlist] + set num_missing_encatts [llength $missing_enc_atts] + set posnum 0 + + # Decide if a motor configuration should be generated. + if [subset $REQ_ATTLIST $mot_attlist] { + set mk_config 1 + } else { + if {$num_missing_encatts == 0} { + set mk_config 1 + } else { + set mk_config 0 + } + } + + # Does this motor have an absolute encoder? + if {$num_missing_encatts == 0} { + set absenc 1 + } else { + set absenc 0 + } + + # Decide if a list missing attributes with default values should be written. + if [subset $ENCMOT_ATTLIST $mot_attlist] { + set mk_missing_atts 1 + } elseif [subset $MOT_ATTLIST $mot_attlist] { + set mk_missing_atts 1 + } else { + set mk_missing_atts 0 + } + + # Assume that the values of any attributes we don't recognise are encoder + # readings for a "posit" motor. + set posit_list {} + foreach att [setdiff $mot_attlist $ALL_ATTRIBUTES] { + incr posnum + lappend posit_list "posit_${posnum} \$${mn}_$att" + } + + # Generate a motor configuration and/or a list of missing attributes. + if ${mk_config} { + mk_motconf $mn $fhmc $absenc $posnum posit_list + puts $fhr "Configured $mn" + incr MOTCFG_CNT + } else { + set missing_atts [lsort [concat $missing_enc_atts [setdiff $::REQ_ATTLIST [array names $mn]]]] + puts $fhe "$mn attributes missing: $missing_atts" + incr FAILED_MOTCFG_CNT + incr ERRCNT + if {$mk_missing_atts} { + set undef_atts [setdiff [lsort "absenchome $SICS_CFG_ATTLIST"] $mot_attlist] + foreach att $undef_atts { + set attval [gen_attval $mn $att] + if {$attval != "NOATT"} { + lappend missing_attlist "${mn}_$att,$attval" + } + } + } + } +} + +# If there are any missing attributes then write them to a file. +if [info exists missing_attlist] { + set fh [open "missing_attlist.csv" "w"] + foreach attval [lsort $missing_attlist] { + puts $fh $attval + } + close $fh + puts "Generated list of missing attributes in missing_attlist.csv. You should rename this if you want to keep it." +} + +# The SICS init code calls motor_set_sobj_attributes. It can be redefined in +# the motor_configuration.tcl file. +puts $fhmc "proc motor_set_sobj_attributes {} {}" +puts stderr "Generated $MOTCFG_CNT motor driver configurations" +puts $fhr "Generated $MOTCFG_CNT motor driver configurations" +puts stderr "Found $FAILED_MOTCFG_CNT incomplete motor configurations. See ${scriptname}_errors.log and ${scriptname}_report.log" +puts $fhe "Found $FAILED_MOTCFG_CNT incomplete motor configurations" + +close $fhmc +close $fhr +close $fhe + +if {$ERRCNT > 0} { + puts stderr "Finished with $::ERRCNT errors" +} diff --git a/site_ansto/instrument/util/genmotconf_procs.tcl b/site_ansto/instrument/util/genmotconf_procs.tcl new file mode 100755 index 00000000..478a9ad2 --- /dev/null +++ b/site_ansto/instrument/util/genmotconf_procs.tcl @@ -0,0 +1,231 @@ + +# @brief Parse motor attribute file and make list of motor attributes and controllers +# @param fname Name of two column csv file of motor attribute names and values +# @param fhr Report log file-handle. +# @param fhe Error log file-handle. +# Requires The global ::ENC_ATTLIST +# Creates the following globals, +# ::mn(matt) is an array of motor attributes where mn = motor name and matt = the attribute name +# ::mn_encatts(matt) is an array of encoder attributes where mn = motor name and matt = an attributed from ENC_ATTLIST +# ::controllers(cn) Counts number of axes used on each controller, cn = controller name. +# ::motor_attcnt(mn) The keys of this array provide a list of motor names and each value is a count of attributes found for each motor. +proc parse_file {fname fhr fhe} { + puts $fhr "\nPARSE '$fname'" + set lcount 0 + set fh [open $fname "r"] + puts $fhr "Create a per motor attribute dictionary for each attribute in the following list of required attributes," + while {[gets $fh line] >= 0} { + incr lcount + foreach {name val} [split $line {,}] {} + if [regexp {(_?[a-zA-Z][a-zA-Z0-9]*[0-9]*)_([a-zA-Z0-9_]+)} $name rem mn matt] { + puts $fhr "Processing '$line'" + if {[lsearch $::ENC_ATTLIST $matt] != -1} { + puts $fhr "Add ::${mn}_encatts($matt) = $val" + set ::${mn}_encatts($matt) $val + } else { + puts $fhr "Add ::${mn}($matt) = $val" + set ::${mn}($matt) $val + } + incr ::motor_attcnt($mn) + if { $matt == "mc" } { + incr ::controllers($val) + } + } else { + incr ::ERRCNT + puts $fhe "Failed to match '$line'" + } + } + close $fh + puts -nonewline $fhr "CONTROLLER USAGE COUNT: " + foreach {cont count} [array get ::controllers] { + puts -nonewline $fhr "$cont count=$count; " + } + puts $fhr "" + puts $fhr "PARSED $lcount lines in '$fname'; failed $::ERRCNT lines" + puts $fhe "$::ERRCNT ERRORS IN PARSE STAGE '$fname'. PROCESSED $lcount lines" +} + +# @brief Generate motor configuration file header and make asyncqueues +# @param fhmc Motor configuration file handle. +# Requires the following globals, +# ::argv0, ::argv, ::file_list, and ::controllers +proc mk_cfg_header {fhmc} { + puts $fhmc "#### SICS motor driver configuration ####" + puts $fhmc "# Generated by: $::argv0 $::argv" + puts $fhmc "# Date: [clock format [clock seconds] -format %Y-%m-%dT%H:%M:%S]" + puts $fhmc "# Generated from the following files," + foreach f $::file_list { + incr fcntr + puts $fhmc "# file$fcntr: $f" + } + puts $fhmc "" + puts $fhmc "# Load motor driver configuration parameters" + puts $fhmc "set flist \[list\\" + foreach f $::file_list { + puts $fhmc " \{$f\}\\" + } + puts $fhmc " \]" +puts $fhmc { +foreach fattfile $flist { + if [catch { + set fattpath config/motors/$fattfile + set fh [open $fattpath RDONLY] + while {[gets $fh line] >= 0} { + eval "set [split $line {,}]" + } + close $fh + } msg] { + clientput ERROR: $msg + } +} + +} +puts $fhmc {set sim_mode [SplitReply [motor_simulation]]} + +# Setup addresses of Galil DMC2280 controllers. +puts $fhmc { +if {$sim_mode == true} { + set motor_driver_type asim +} else { + set motor_driver_type DMC2280 +} +} +puts $fhmc "if {\$sim_mode == false} \{" +foreach cont [array names ::controllers] { + set index [string toupper $cont] + puts $fhmc " [subst -nocommands {MakeAsyncQueue $cont DMC2280 [dict get \$::MOTOR_HOSTPORT $index HOST] [dict get \$::MOTOR_HOSTPORT $index PORT]}]" +} +puts $fhmc \} + +} + +# @brief Generate motor driver configuration +# @param mot Motor name +# @param fh Motor configuration file handle +# @param absEnc boolean, generate absolute encoder configuration if true. +# @param posnum Number of discrete positions +# @param posit_list Name of global list of name value pairs, "posit_n pos". Can be empty. +proc mk_motconf {mot fh absEnc posnum posit_list} { + set mc [set ::${mot}(mc)] + set axis [set ::${mot}(axis)] + set units ${mot}_units + set hardlowerlim ${mot}_rev_lim + set hardupperlim ${mot}_fwd_lim + set softlowerlim ${mot}_rev_lim + set softupperlim ${mot}_fwd_lim + set maxSpeed ${mot}_maxspeed + set maxAccel ${mot}_maxaccel + set maxDecel ${mot}_maxdecel + set stepsPerX ${mot}_steps_per_x + set cntsPerX ${mot}_cnts_per_x + set absEncHome ${mot}_absenchome + set home ${mot}_home + set part ${mot}_part + set long_name $mot + set description "$mot configuration" + + puts $fh "# $description" + puts $fh "Motor $mot \$motor_driver_type \[params\\" + puts $fh " asyncqueue $mc\\" + puts $fh " axis $axis\\" + puts $fh " units \$$units\\" + puts $fh " hardlowerlim \$$hardlowerlim\\" + puts $fh " hardupperlim \$$hardupperlim\\" + puts $fh " maxSpeed \$$maxSpeed\\" + puts $fh " maxAccel \$$maxAccel\\" + puts $fh " maxDecel \$$maxDecel\\" + puts $fh " stepsPerX \$$stepsPerX\\" + puts $fh " posit_count $posnum\\" + foreach positline $::posit_list { + puts $fh " $positline\\" + } + if {$absEnc} { + puts $fh " absEnc 1\\" + puts $fh " absEncHome \$$absEncHome\\" + puts $fh " cntsPerX \$$cntsPerX\]" + } else { + puts $fh " absEnc 0\]" + } + puts $fh "$mot softlowerlim \$$softlowerlim" + puts $fh "$mot softupperlim \$$softupperlim" + puts $fh "$mot home \$$home" + puts $fh "$mot part \$$part" + puts $fh "$mot long_name $long_name" + puts $fh "" +} + + + +################################################################################ +# UTILITY FUNCTIONS +################################################################################ + +# @brief Difference between sets A and B, ie A\B +# Invalid sets are allowed. Doesn't check for repetition. +# @return list of elements in A but not in B +proc setdiff {A B} { + set diff {} + foreach e $A { + if { [lsearch $B $e] == -1 } { + lappend diff $e + } + } + return $diff +} + +# @brief Tests if A is subset of B +# The sets must be sorted +# Doesn't check if sets are valid, eg repetition is allowed. +proc subset {A B} { + set Alen [llength $A] + set Blen [llength $B] + if { $Alen > $Blen } { + return false + } + set Aend [expr {$Alen - 1}] + set Bend [expr {$Blen - 1}] + set asi 0 + set aei 0 + set bsi 0 + set bei 0 + while { $asi <= [expr {$Aend - $aei}] } { +# puts "A = [lrange $A $asi end-$aei]" + set firstmatch false + set lastmatch false + while { $bsi <= [expr {$Bend - $bei}] } { +# puts " B = [lrange $B $bsi end-$bei]" + if { $firstmatch == false } { + set aval [lindex $A $asi] + set bval [lindex $B $bsi] + if { $aval == $bval} { + set firstmatch true +# puts " firstmatch = $firstmatch" + } elseif {$aval < $bval} { + return false + } + incr bsi + } + if { $lastmatch == false } { + set aval [lindex $A end-$aei] + set bval [lindex $B end-$bei] + if { $aval == $bval } { + set lastmatch true +# puts " lastmatch = $lastmatch" + } elseif {$aval > $bval} { + return false + } + incr bei + } + if { ($firstmatch == true) && ($lastmatch == true) } { + break + } + } + if { ($firstmatch == false) && ($lastmatch == false) } { + return false + } + incr asi + incr aei + } + + return true +}