# sim_lib.tcl: haltcl procs for sim configurations

#----------------------------------------------------------------------
# Notes (Joints-Axes):
#   1) if  ::KINS(KINEMATICS) exists:
#        loadrt the kins using any included parameters
#        example: for inifile item:
#           [KINS]KINEMATICS = trivkins coordinates=XZ kinstype=BOTH
#        use:
#           loadrt trivkins coordinates=xz kinstype=BOTH
#      else:
#           loadrt trivkins
#
#   2) NB: If $::KINS(KINEMATICS) specifies coordinates=, the
#          coordinates must agree with ::TRAJ(COORDINATES)
#
#   3) if known kins (trivkins) and xyz, make hypotenuse velocity
#      pins for xy,xyz
#----------------------------------------------------------------------

proc indices_for_trivkins {axes} {
  # ref: src/emc/kinematics/trivkins.c
  if {"$axes" == ""} {set axes {x y z a b c u v w}}
  set i 0
  foreach a [string tolower $axes] {
     # assign to consecutive joints:
     set ::SIM_LIB(jointidx,$a) $i
     incr i
  }
} ;# indices_for_trivkins

proc get_traj_coordinates {} {
  # initraj.cc: coordinates may be with or without spaces X Z or XZ
  # convert either form to list like {x z}
  set coordinates [lindex $::TRAJ(COORDINATES) 0]
  set coordinates [string map {" " "" "\t" ""} $coordinates]
  set coordinates [split $coordinates ""]
  return [string tolower $coordinates]
} ;# get_traj_coordinates

proc check_ini_items {} {
  foreach {section item} {KINS KINEMATICS
                          KINS JOINTS
                          TRAJ COORDINATES
                         } {
    if ![info exists ::${section}($item)] {
      return -code error "Missing inifile item: \[$section\]$item"
    }
  }

  if {   [info exists ::DISPLAY(LATHE)]
      && [lsearch $::KINS(KINEMATICS) trivkins] >= 0
     } {
     # reject historical lathe config using default trivkins coordinates (all)
     if {[string first "=" $::KINS(KINEMATICS)] < 0} {
       set msg "trivkins lathe config must specify coordinates= "
       set msg "$msg\n(typically use \[KINS]KINEMATICS trivkins coordinates=XZ)"
       return -code error "$msg"
     }
  }

  set n_extrajoints 0
  if [info exists ::EMCMOT(EMCMOT)] {
    set mot [split [lindex $::EMCMOT(EMCMOT) 0]]
    if {[string first motmod $mot] >= 0} {
      foreach item $mot {
        if {[string first num_extrajoints= $item] < 0} continue
          set n_extrajoints [lindex [split $item =] end]
      } ;# foreach
    }
  }

  set kins [split [lindex $::KINS(KINEMATICS) 0]]
  if {[string first trivkins $kins] >= 0} {
    foreach item $kins {
      if {[string first coordinates= $item] < 0} continue
      set     tcoords [lindex [split $item =] end]
      set len_tcoords [string len $tcoords]
      set expected_joints [expr $len_tcoords + $n_extrajoints]
      if {$expected_joints != $::KINS(JOINTS)} {
          set m "\ncheck_ini_items: WARNING:\n"
          set m "$m  trivkins coordinates=$tcoords specifies $len_tcoords joints\n"
          set m "$m  trivkins extrajoints=$n_extrajoints\n"
          set m "$m  trivkins totaljoints=$expected_joints\n"
          set m "$m  !!! but \[KINS\]JOINTS=$::KINS(JOINTS)\n"
          puts stderr $m
      }
    } ;# foreach
  }
  return
} ;# check_ini_items

proc setup_kins {axes} {
  if ![info exists ::KINS(KINEMATICS)] {
    puts stderr "setup_kins: NO \[KINS\]KINEMATICS, trying default trivkins"
    loadrt trivkins
    return
  }
  set kins_kinematics [lindex $::KINS(KINEMATICS) end]
  set cmd "loadrt $kins_kinematics" ;# may include parms
  set kins_module [lindex $kins_kinematics 0]

  puts stderr "setup_kins: cmd=$cmd"
  if [catch {eval $cmd} msg] {
    puts stderr "\nmsg=$msg\n"
  }

  # set up axis indices for known kins
  switch $kins_module {
    trivkins {indices_for_trivkins $axes}
    default  {
      puts stderr "setup_kins: unknown \[KINS\]KINEMATICS=<$::KINS(KINEMATICS)>"
    }
  }
} ;# setup_kins

proc core_sim {axes
               number_of_joints
               servo_period
               {base_period 0}
               {emcmot motmod}
              } {
  # adapted as haltcl proc from core_sim.hal
  # note: with default emcmot==motmot,
  #       thread will not be added for (default) base_pariod == 0

  setup_kins $axes

  if {"$emcmot" == "motmod"} {
    if [catch {loadrt $emcmot \
                      base_period_nsec=$base_period \
                      servo_period_nsec=$servo_period \
                      num_joints=$number_of_joints} msg
       ] {
       # typ: too many joints attempted
       puts stderr "\n"
       puts stderr "core_sim: loadrt $emcmot FAIL"
       puts stderr "     msg=$msg\n"
       exit 1
    }
  } else {
     # known special case with additional parameter:
     #       unlock_joint_mask=0xNN
     #       num_extrajoints=n
     set module  [split [lindex $emcmot 0]]
     set modname [lindex $module 0]
     set modparm [lreplace $module 0 0]
     if [catch {eval loadrt $modname $modparm \
                  base_period_nsec=$base_period \
                  servo_period_nsec=$servo_period \
                  num_joints=$number_of_joints} msg
        ] {
        puts stderr "\n"
        puts stderr "core_sim:unhandled emcmot<$emcmot>"
        puts stderr "         modname=$modname"
        puts stderr "         modparm=$modparm"
        puts stderr "     msg=$msg\n"
        exit 1
     }
  }

  addf motion-command-handler servo-thread
  addf motion-controller      servo-thread

  set pid_names ""
  set mux_names ""
  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    set pid_names "${pid_names},J${jno}_pid"
    set mux_names "${mux_names},J${jno}_mux"
  }
  set pid_names [string trimleft $pid_names ,]
  set mux_names [string trimleft $mux_names ,]
  loadrt pid  names=$pid_names
  loadrt mux2 names=$mux_names

  # pid components
  # The pid comp is used as a pass-thru device (FF0=1,all other gains=0)
  # to emulate the connectivity of a servo system
  # (e.g., no short-circuit connection of motor-pos-cmd to motor-pos-fb)
  foreach cname [split $pid_names ,] {
    addf ${cname}.do-pid-calcs servo-thread
    # FF0 == 1 for pass-thru, all others 0
    do_setp ${cname}.FF0 1.0
    foreach pin {Pgain Dgain Igain FF1 FF2} { do_setp ${cname}.$pin 0 }
  }

  # mux components
  # The mux comp is used as a sample-hold to simulate a machine
  # with encoders that measure output position when power
  # is not applied to the motors or controllers
  foreach cname [split $mux_names ,] {
    addf $cname servo-thread
  }

  # signal connections:
  net estop:loop <= iocontrol.0.user-enable-out
  net estop:loop => iocontrol.0.emc-enable-in

  net tool:prep-loop <= iocontrol.0.tool-prepare
  net tool:prep-loop => iocontrol.0.tool-prepared

  net tool:change-loop <= iocontrol.0.tool-change
  net tool:change-loop => iocontrol.0.tool-changed

  net sample:enable <= motion.motion-enabled

  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    net sample:enable => J${jno}_mux.sel

    net J${jno}:enable  <= joint.$jno.amp-enable-out
    net J${jno}:enable  => J${jno}_pid.enable

    net J${jno}:pos-cmd <= joint.$jno.motor-pos-cmd
    net J${jno}:pos-cmd => J${jno}_pid.command

    net J${jno}:on-pos  <= J${jno}_pid.output
    net J${jno}:on-pos  => J${jno}_mux.in1 ;# pass thru when motion-enabled

    net J${jno}:pos-fb  <= J${jno}_mux.out
    net J${jno}:pos-fb  => J${jno}_mux.in0 ;# hold position when !motion-enabled
    net J${jno}:pos-fb  => joint.$jno.motor-pos-fb
  }
} ;# core_sim

proc make_ddts {number_of_joints} {
  # make vel,accel ddts and signals for all joints
  # if xyz, make hypotenuse xy,xyz vels

  set ddt_names ""
  set ddt_ct 0
  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    incr ddt_ct 2
    set ddt_names "${ddt_names},J${jno}_vel,J${jno}_accel"
  }
  set ddt_names [string trimleft $ddt_names ,]
  loadrt ddt names=$ddt_names
  foreach cname [split $ddt_names ,] {
    addf $cname servo-thread
  }

  # joint vel,accel signal connections:
  set ddt_ct 0
  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    incr ddt_ct 2
    net J${jno}:pos-fb   => J${jno}_vel.in ;# net presumed to exist
    net J${jno}:vel      <= J${jno}_vel.out
    net J${jno}:vel      => J${jno}_accel.in
    net J${jno}:acc      <= J${jno}_accel.out
  }

  set has_xyz 1
  foreach letter {x y z} {
    if ![info exists ::SIM_LIB(jointidx,$letter)] {
      set has_xyz 0
      break
    }
  }
  if $has_xyz {
    loadrt hypot names=hyp_xy,hyp_xyz ;# vector velocities
    addf hyp_xy  servo-thread
    addf hyp_xyz servo-thread
    net J$::SIM_LIB(jointidx,x):vel <= J$::SIM_LIB(jointidx,x)_vel.out
    net J$::SIM_LIB(jointidx,x):vel => hyp_xy.in0
    net J$::SIM_LIB(jointidx,x):vel => hyp_xyz.in0

    net J$::SIM_LIB(jointidx,y):vel <= J$::SIM_LIB(jointidx,y)_vel.out
    net J$::SIM_LIB(jointidx,y):vel => hyp_xy.in1
    net J$::SIM_LIB(jointidx,y):vel => hyp_xyz.in1

    net J$::SIM_LIB(jointidx,z):vel <= J$::SIM_LIB(jointidx,z)_vel.out
    net J$::SIM_LIB(jointidx,z):vel => hyp_xyz.in2

    net xy:vel   => hyp_xy.out
    net xyz:vel  <= hyp_xyz.out
  }
} ;# make_ddts

proc use_hal_manualtoolchange {} {
  # adapted as haltcl proc from axis_manualtoolchange.hal
  loadusr -W hal_manualtoolchange

  # disconnect if previously connected:
  unlinkp iocontrol.0.tool-change
  unlinkp iocontrol.0.tool-changed
  # remove signal with no connections:
  delsig tool:change-loop

  net tool:change <= iocontrol.0.tool-change
  net tool:change => hal_manualtoolchange.change

  net tool:changed <= hal_manualtoolchange.changed
  net tool:changed => iocontrol.0.tool-changed

  net tool:prep-number <= hal_manualtoolchange.number
  net tool:prep-number => iocontrol.0.tool-prep-number
} ;# use_hal_manualtoolchange

proc simulated_home {number_of_joints} {
  # uses sim_home_switch component
  set switch_names ""
  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    set switch_names "${switch_names},J${jno}_switch"
  }
  set switch_names [string trimleft $switch_names ,]
  loadrt sim_home_switch names=$switch_names
  foreach cname [split $switch_names ,] {
    addf $cname servo-thread
  }

  for {set jno 0} {$jno < $number_of_joints} {incr jno} {
    # add pin to pre-existing signal:
    net J${jno}:pos-fb => J${jno}_switch.cur-pos

    net J${jno}:homesw <= J${jno}_switch.home-sw
    net J${jno}:homesw => joint.$jno.home-sw-in

    # set sim_home_switch .hysteresis,.home-pos pins
    # according to traj units and joint type
    if ![info exists ::JOINT_[set jno](TYPE)] {
      # use component defaults
    } else {
      if {"[set ::JOINT_[set jno](TYPE)]" == "ANGULAR"} {
        # use component defaults
      } else {
        if ![info exists ::TRAJ(LINEAR_UNITS)] {
          # use component defaults
        } else {
          switch $::TRAJ(LINEAR_UNITS) {
            in - inch - imperial {
              do_setp J${jno}_switch.hysteresis 0.05
              do_setp J${jno}_switch.home-pos   0.10
            }
            default { # use component default }
          }
        }
        if {    [info exists ::JOINT_[set jno](HOME_SEARCH_VEL)]
            &&  [set ::JOINT_[set jno](HOME_SEARCH_VEL)] < 0} {
            do_setp J${jno}_switch.home-pos -[getp J${jno}_switch.home-pos]
        }
      }
    } ;# type
    if [info exists ::JOINT_[set jno](HOME_USE_INDEX)] {
      if [set ::JOINT_[set jno](HOME_USE_INDEX)] {
        # Note: use default for joint.${jno}.index-delay-ms
        net J${jno}:index-enable <= joint.${jno}.index-enable
        net J${jno}:index-enable => J${jno}_switch.index-enable
      }
    }
  } ;# for
} ;# simulated_home

proc sim_spindle {} {
  # adapted as haltcl proc from sim_spindle_encoder.hal
  # simulated spindle encoder (for spindle-synced moves)
  loadrt sim_spindle names=sim_spindle
  do_setp sim_spindle.scale 0.01666667

  loadrt limit2  names=limit_speed
  loadrt lowpass names=spindle_mass
  loadrt near    names=near_speed
  loadrt scale names=rpm_rps

  setp rpm_rps.gain .0167

  # this limit doesn't make any sense to me:
  do_setp limit_speed.maxv 5000.0 ;# rpm/second

  # encoder reset control
  # hook up motion controller's sync output
  net spindle-index-enable <=> spindle.0.index-enable
  net spindle-index-enable <=> sim_spindle.index-enable

  # report our revolution count to the motion controller
  net spindle-pos <= sim_spindle.position-fb
  net spindle-pos => spindle.0.revs

  # simulate spindle mass
  do_setp spindle_mass.gain .07

  # spindle speed control
  net spindle-speed-cmd <= spindle.0.speed-out
  net spindle-speed-cmd => limit_speed.in
  net spindle-speed-cmd => near_speed.in1

  net spindle-speed-limited <= limit_speed.out
  net spindle-speed-limited => sim_spindle.velocity-cmd
  net spindle-speed-limited => spindle_mass.in

  # for spindle velocity estimate
  net spindle-rpm-filtered <= spindle_mass.out
  net spindle-rpm-filtered     rpm_rps.in
  net spindle-rps-filtered     rpm_rps.out    spindle.0.speed-in 
  net spindle-rpm-filtered => near_speed.in2

  # at-speed detection
  do_setp near_speed.scale 1.1
  do_setp near_speed.difference 10

  net spindle-at-speed <= near_speed.out
  net spindle-at-speed => spindle.0.at-speed

  net spindle-orient <= spindle.0.orient
  net spindle-orient <= spindle.0.is-oriented

  addf limit_speed  servo-thread
  addf spindle_mass servo-thread
  addf rpm_rps servo-thread
  addf near_speed   servo-thread
  addf sim_spindle  servo-thread
} ;# sim_spindle

proc save_hal_cmds {savefilename {options ""} } {
  set tmpfile /tmp/save_hal_cmds_tmp
  set date [clock format [clock seconds]]
  set script [info script]
  set save_arg all  ;# suffices if only basic_sim in use
  if {[llength $::HAL(HALFILE)] > 1} {
    set save_arg allu ;# do *all* unconnected pins including
                      ;# pins from other HALFILEs
  }
  set fd [open $savefilename w] ;# overwrite any existing file
  puts $fd "# $date
#
# This file:    $savefilename
# Created by:   $script
# With options: $::argv
# From inifile: $::env(INI_FILE_NAME)
# Halfiles:     $::HAL(HALFILE)
#
# This file contains the hal commands produced by [file tail $script]
# (and any hal commands executed prior to its execution).
# ------------------------------------------------------------------
# To use $savefilename in the original inifile (or a copy of it),
# edit to change:
#     \[HAL\]
#     HALFILE = LIB:basic_sim.tcl parameters
# to:
#     \[HAL\]
#     HALFILE = $savefilename
#
# Notes:
#  1) Inifile Variables substitutions specified in the inifile
#     and interpreted by halcmd are automatically substituted
#     in the created halfile ($savefilename).
#  2) Input pins connected to a signal with no writer are
#     not included in the setp listings herein so must be added
#     manually
#
"
  if {[lsearch $options use_hal_manualtoolchange] >= 0} {
    puts $fd "# user space components"
    puts $fd "loadusr -W hal_manualtoolchange"
    puts $fd ""
  }

  hal save $save_arg $tmpfile

  set ftmp [open $tmpfile r]
  set gave_msg 0
  set setp_fmt   "%-3s %-30s %s"
  while {![eof $ftmp]} {
    gets $ftmp line
    if {([string first "unconnected pin values" $line] >0) && !$gave_msg} {
      set gave_msg 1
      puts $fd "# Note: ALL unconnected pins follow"
      puts $fd "#       (includes pins using default with no explicit setp command)"
    } else {
      scan $line "%s %s %s" cmd arg1 remainder
      switch $cmd {
        setp    {puts $fd [format $setp_fmt $cmd $arg1 $remainder]}
        loadrt  {
                 if {   [string first tpmod   [list $line]] >= 0
                     || [string first homemod [list $line]] >= 0
                    } {
                    puts $fd "#preloaded module: $line"
                 } else {
                    puts $fd $line
                 }
                }
        default {puts $fd $line}
      }
    }
  } ;# while
  close $ftmp
  file delete $tmpfile
  if {("$save_arg" != "allu") && [info exists ::SIM_LIB(setp_list)]} {
    puts $fd "# setp commands for unconnected input pins"
    foreach {pname value} $::SIM_LIB(setp_list) {
       puts $fd [format $setp_fmt setp $pname $value]
    }
  }
  close $fd
} ;# save_hal_cmds

proc do_setp {pname value} {
  setp $pname $value
  lappend ::SIM_LIB(setp_list) $pname $value
} ;# do_setp
