Merging release 2.0 branch with CVS trunk
r2601 | ffr | 2008-05-30 10:26:57 +1000 (Fri, 30 May 2008) | 2 lines
This commit is contained in:
committed by
Douglas Clowes
parent
4a937e1608
commit
0749b0effa
@@ -1,25 +1,235 @@
|
||||
# Some useful functions for SICS configuration.
|
||||
|
||||
# $Revision: 1.10 $
|
||||
# $Date: 2007-11-05 02:11:41 $
|
||||
# $Revision: 1.11 $
|
||||
# $Date: 2008-05-30 00:26:57 $
|
||||
# Author: Ferdi Franceschini (ffr@ansto.gov.au)
|
||||
# Last revision by $Author: ffr $
|
||||
|
||||
source util/extra_utility.tcl
|
||||
source util/motor_utility.tcl
|
||||
source util/command.tcl
|
||||
namespace eval environment { }
|
||||
# @brief Return the number of sensors for a given environment object
|
||||
proc ::environment::getnumsensors {sobj} {
|
||||
if [ catch {
|
||||
if {[SplitReply [environment_simulation]]=="true"} {
|
||||
set ns [getatt $sobj numsensors]
|
||||
return $ns
|
||||
} else {
|
||||
set ns [SplitReply [$sobj numsensors]]
|
||||
return $ns
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# @brief Return the list of sensor names for the given environment object
|
||||
proc ::environment::getsensorlist {sobj} {
|
||||
if [ catch {
|
||||
if {[SplitReply [environment_simulation]]=="true"} {
|
||||
set sl [ split [getatt $sobj sensorlist] , ]
|
||||
return $sl
|
||||
} else {
|
||||
set sl [ split [SplitReply [$sobj sensorlist]] , ]
|
||||
return $sl
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# @brief Create SICS variables for the environment controller
|
||||
# sensor readings which we use for feedback in the GumTree interface.
|
||||
#
|
||||
# These sensor-reading variables will be attached to the hdb tree
|
||||
# and updated at regular intervals
|
||||
#
|
||||
# @param sobj, SICS environment controller object name.
|
||||
# @return A space separated list of the sensor-reading variable names.
|
||||
proc ::environment::mkSensors {sobj} {
|
||||
if [ catch {
|
||||
set sim_mode [SplitReply [environment_simulation]]
|
||||
set sensors [::environment::getsensorlist $sobj]
|
||||
foreach sensor $sensors {
|
||||
proc ::environment::${sobj}_${sensor} {} [ subst -nocommands {
|
||||
if {$sim_mode == "true"} {
|
||||
return [expr rand()]
|
||||
} else {
|
||||
return [SplitReply [$sobj $sensor]]
|
||||
}
|
||||
}]
|
||||
set ss_script ::environment::${sobj}_${sensor}
|
||||
publish $ss_script user
|
||||
sicslist setatt $ss_script access read_only
|
||||
sicslist setatt $ss_script privilege internal
|
||||
sicslist setatt $ss_script long_name value
|
||||
sicslist setatt $ss_script dtype float
|
||||
sicslist setatt $ss_script dlen 1
|
||||
sicslist setatt $ss_script data true
|
||||
sicslist setatt $ss_script nxsave true
|
||||
sicslist setatt $ss_script mutable true
|
||||
sicslist setatt $ss_script control true
|
||||
sicslist setatt $ss_script units [getatt $sobj units]
|
||||
sicslist setatt $ss_script klass sensor
|
||||
sicslist setatt $ss_script kind script
|
||||
append sensorlist [subst {
|
||||
$sensor {
|
||||
macro { $ss_script }
|
||||
}
|
||||
}]
|
||||
}
|
||||
return $sensorlist
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# @brief Create the information structure
|
||||
#
|
||||
# @param sobj, name of SICS environment controller object
|
||||
# @param paramlist a nested list of parameters and their attributes\n
|
||||
# eg, {heateron {priv user} range {priv manager}}\n
|
||||
# this adds the heateron and range parameters with their access privilege.\n
|
||||
# Note: The priv attribute is mandatory.
|
||||
#
|
||||
# eg ::environment::mkenvinfo tc1 {heateron {priv user} range {priv manager}}
|
||||
proc ::environment::mkenvinfo {sobj paramlist} {
|
||||
lappend paramlist controlsensor {priv user}
|
||||
if [ catch {
|
||||
# Create polling procedure to update hdb sensor data nodes.
|
||||
# proc ::environment::${sobj}_poll [subst {{sobj $sobj}}] {
|
||||
# set sim_mode [SplitReply [environment_simulation]]
|
||||
# set sensors [::environment::getsensorlist $sobj]
|
||||
# if {$sim_mode == "true"} {
|
||||
# foreach ss $sensors {
|
||||
# ${sobj}_${ss} [expr rand()]
|
||||
# }
|
||||
# } else {
|
||||
# foreach ss $sensors {
|
||||
# ${sobj}_${ss} [SplitReply [$sobj $ss]]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
set setpoint_script ::environment::${sobj}_setpoint
|
||||
proc $setpoint_script [subst {{val "@none"} {_sobj $sobj}}] {
|
||||
if [catch {
|
||||
if {[SplitReply [environment_simulation]]=="true"} {
|
||||
if {$val=="@none"} {
|
||||
return [SplitReply [${_sobj}]]
|
||||
} else {
|
||||
${_sobj} $val
|
||||
}
|
||||
} else {
|
||||
if {$val=="@none"} {
|
||||
return [SplitReply [${_sobj} setpoint]]
|
||||
} else {
|
||||
${_sobj} $val
|
||||
}
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode == "NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
publish $setpoint_script user
|
||||
sicslist setatt $setpoint_script privilege internal
|
||||
sicslist setatt $setpoint_script access rw
|
||||
sicslist setatt $setpoint_script long_name setpoint
|
||||
sicslist setatt $setpoint_script dtype float
|
||||
sicslist setatt $setpoint_script dlen 1
|
||||
sicslist setatt $setpoint_script data false
|
||||
sicslist setatt $setpoint_script nxsave false
|
||||
sicslist setatt $setpoint_script mutable false
|
||||
sicslist setatt $setpoint_script control true
|
||||
sicslist setatt $setpoint_script units K
|
||||
sicslist setatt $setpoint_script klass sensor
|
||||
sicslist setatt $setpoint_script kind script
|
||||
lappend env_macrolist $setpoint_script
|
||||
|
||||
foreach {param attlist} $paramlist {
|
||||
array set atthash $attlist
|
||||
proc ::environment::${sobj}_${param} [subst {{val "@none"} {_sobj $sobj} {_param $param}}] {
|
||||
if {[SplitReply [environment_simulation]]=="true"} {
|
||||
if {$val=="@none"} {
|
||||
return [getatt ${_sobj} ${_param}]
|
||||
} else {
|
||||
sicslist setatt ${_sobj} ${_param} $val
|
||||
}
|
||||
} else {
|
||||
if {$val=="@none"} {
|
||||
return [SplitReply [${_sobj} ${_param}]]
|
||||
} else {
|
||||
${_sobj} ${_param} $val
|
||||
}
|
||||
}
|
||||
}
|
||||
set ctrlss_script ::environment::${sobj}_${param}
|
||||
publish $ctrlss_script user
|
||||
sicslist setatt $ctrlss_script long_name ${param}
|
||||
sicslist setatt $ctrlss_script kind script
|
||||
sicslist setatt $ctrlss_script privilege $atthash(priv)
|
||||
sicslist setatt $ctrlss_script klass @none
|
||||
sicslist setatt $ctrlss_script data false
|
||||
sicslist setatt $ctrlss_script control true
|
||||
sicslist setatt $ctrlss_script nxsave false
|
||||
sicslist setatt $ctrlss_script dtype "text"
|
||||
sicslist setatt $ctrlss_script dlen 10
|
||||
sicslist setatt $ctrlss_script access rw
|
||||
lappend env_macrolist $ctrlss_script
|
||||
}
|
||||
|
||||
# Create environment information structure for hdb
|
||||
set env_name [getatt $sobj environment_name]
|
||||
eval [subst {
|
||||
proc ::${sobj}_dict {} {
|
||||
return {
|
||||
NXenvironment {
|
||||
$env_name {
|
||||
macro {$env_macrolist}
|
||||
NXsensor {
|
||||
[::environment::mkSensors $sobj]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ]
|
||||
publish ::${sobj}_dict mugger
|
||||
sicslist setatt ::${sobj}_dict kind hdb_subtree
|
||||
sicslist setatt ::${sobj}_dict klass environment
|
||||
sicslist setatt ::${sobj}_dict privilege user
|
||||
sicslist setatt ::${sobj}_dict long_name tempone
|
||||
sicslist setatt ::${sobj}_dict data true
|
||||
sicslist setatt ::${sobj}_dict control true
|
||||
sicslist setatt ::${sobj}_dict nxsave true
|
||||
sicslist setatt ::${sobj}_dict sdsinfo ::nexus::environment_controller::sdsinfo
|
||||
sicslist setatt ::${sobj}_dict savecmd ::nexus::environment_controller::save
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# Returns attribute name and value
|
||||
proc getatt {sicsobj att} {
|
||||
if [catch {
|
||||
lindex [split [tolower_sicslist $sicsobj $att] =] 1
|
||||
} reply ] {
|
||||
return -code error "[info level 0]\n$reply"
|
||||
return -code error $reply
|
||||
} else {
|
||||
return $reply
|
||||
}
|
||||
}
|
||||
|
||||
# @brief Determine if a SICS object implements the drivable interface.
|
||||
#
|
||||
# @param sicsobj, Name of a SICS object
|
||||
# @return 1 if drivable, otherwise 0
|
||||
proc is_drivable {sicsobj} {
|
||||
if [catch {
|
||||
getatt $sicsobj drivable
|
||||
@@ -105,14 +315,21 @@ namespace eval utility {
|
||||
variable sics_port
|
||||
set base_port 60000
|
||||
set currbase $base_port
|
||||
set valbase_port 60010
|
||||
set currvalbase $valbase_port
|
||||
foreach inst $instrument_names {
|
||||
array set sics_port [list\
|
||||
telnet-$inst $currbase\
|
||||
interrupt-$inst [expr {$currbase+1}]\
|
||||
server-$inst [expr {$currbase+2}]\
|
||||
quieck-$inst [expr {$currbase+3}]\
|
||||
telnet-val-$inst $currvalbase\
|
||||
interrupt-val-$inst [expr {$currvalbase+1}]\
|
||||
server-val-$inst [expr {$currvalbase+2}]\
|
||||
quieck-val-$inst [expr {$currvalbase+3}]\
|
||||
]
|
||||
set currbase [expr {$currbase+100}]
|
||||
set currvalbase [expr {$currvalbase+100}]
|
||||
}
|
||||
namespace export instname;
|
||||
namespace export get_portnum;
|
||||
@@ -185,6 +402,18 @@ proc echo {args} {
|
||||
clientput $args
|
||||
}
|
||||
|
||||
# @brief Check if a SICS object or Tcl object exists.
|
||||
#
|
||||
# @param obj, name of a SICS or Tcl object
|
||||
# @return 1 if obj exists otherwise 0
|
||||
proc ::utility::obj_exists {obj} {
|
||||
if { [sicslist match $obj ] != "" || [info exists $obj] } {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::set_sobj_attributes {} {
|
||||
sicslist setatt getinfo privilege internal
|
||||
sicslist setatt setpos privilege internal
|
||||
@@ -235,27 +464,48 @@ proc ::utility::set_motor_attributes {} {
|
||||
}
|
||||
}
|
||||
proc ::utility::set_envcontrol_attributes {} {
|
||||
foreach ec [sicslist type environment_controller] {
|
||||
sicslist setatt $ec kind hobj
|
||||
sicslist setatt $ec data true
|
||||
sicslist setatt $ec control true
|
||||
sicslist setatt $ec nxsave true
|
||||
sicslist setatt $ec privilege user
|
||||
sicslist setatt $ec nxalias $ec
|
||||
sicslist setatt $ec mutable true
|
||||
sicslist setatt $ec klass sample
|
||||
if [ catch {
|
||||
foreach ec [sicslist type environment_controller] {
|
||||
#TODO call mk
|
||||
array unset sobjatt
|
||||
array set sobjatt [attlist $ec]
|
||||
sicslist setatt $ec kind hobj
|
||||
sicslist setatt $ec data true
|
||||
sicslist setatt $ec control false
|
||||
sicslist setatt $ec nxsave true
|
||||
sicslist setatt $ec privilege user
|
||||
sicslist setatt $ec nxalias $ec
|
||||
sicslist setatt $ec mutable true
|
||||
if {[info exists sobjatt(klass)] == 0} {
|
||||
sicslist setatt $ec klass environment
|
||||
}
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# Retuns plain value of hdb node property
|
||||
proc ::utility::hgetplainprop {hpath prop} {
|
||||
return [string trim [lindex [split [hgetprop $hpath $prop] =] 1] ]
|
||||
if [ catch {
|
||||
return [string trim [lindex [split [hgetprop $hpath $prop] =] 1] ]
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
proc ::utility::hlistplainprop {hpath} {
|
||||
return [string trim [join [split [hlistprop $hpath] =] ]]
|
||||
if [ catch {
|
||||
return [string trim [join [split [hlistprop $hpath] =] ]]
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::GetUID {userName} {
|
||||
if [ catch {
|
||||
set fh [open /etc/passwd r]
|
||||
while {[gets $fh tmpName] != -1} {
|
||||
if {1 == [regexp "^$userName:" $tmpName]} {
|
||||
@@ -263,7 +513,12 @@ proc ::utility::GetUID {userName} {
|
||||
return [lindex [split $tmpName :] 2]
|
||||
}
|
||||
}
|
||||
close $fh error "\"$userName\" not found in /etc/passwd"
|
||||
close $fh
|
||||
error "\"$userName\" not found in /etc/passwd"
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
##\brief Determine if list l1 begins with list l2
|
||||
@@ -284,6 +539,7 @@ proc lstarts_with {l1 l2} {
|
||||
proc ::utility::get_portnum {port} {
|
||||
global env tcl_platform
|
||||
variable sics_port
|
||||
if [ catch {
|
||||
if [string is integer $port] {
|
||||
return $port
|
||||
} else {
|
||||
@@ -295,6 +551,10 @@ proc ::utility::get_portnum {port} {
|
||||
return [portnum $port]
|
||||
}
|
||||
}
|
||||
} message] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
##
|
||||
@@ -307,6 +567,202 @@ proc ::utility::callstack {} {
|
||||
}
|
||||
}
|
||||
|
||||
##
|
||||
# @brief Splits "args" list into a head and tail, useful for scripts
|
||||
# where the first argument is a subcommand followed by an argument list.
|
||||
#
|
||||
# Usage: foreach {opt arglist} [::utility::get_opt_arglist $args] {}
|
||||
proc ::utility::get_opt_arglist {args} {
|
||||
if [ catch {
|
||||
if {[llength $args] == 1} {
|
||||
set arguments [lindex $args 0]
|
||||
} else {
|
||||
set arguments $args
|
||||
}
|
||||
set opt [lindex $arguments 0]
|
||||
set arglist [lrange $arguments 1 end]
|
||||
return [list $opt $arglist]
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
# These functions handle a special nested list of name value pairs
|
||||
# which can be represented as an XML element.
|
||||
# Examples
|
||||
# To make a new table you can just create an empty list, eg
|
||||
# set newtable [list ]
|
||||
# You can then fill your new table using tabset, eg
|
||||
# ::utility::tabset newtable a/b/c {values {1 2 3}}
|
||||
# newtable looks like this
|
||||
# a {b {c {values {1 2 3}}}}
|
||||
#
|
||||
# NOTE you can generate the previous table anonymously with
|
||||
# ::utility::tabmktable {a b c values {1 2 3}}
|
||||
# -> a {b {c {values {1 2 3}}}}
|
||||
#
|
||||
# ::utility::tabmktable {NXgeometry geometry NXshape sicsvariable {shape size}}
|
||||
# returns
|
||||
# NXgeometry {geometry {NXshape {sicsvariable {shape size}}}}
|
||||
# ::utility::tabxml hmm_table SAT
|
||||
# ::utility::tabset hmm_table SAT/SPLIT/_ATTLIST_/MIDPOINT 256
|
||||
# ::utility::tabget hmm_table SAT/SPLIT/_ATTLIST_/MIDPOINT
|
||||
# ::utility::tabxml hmm_table SAT
|
||||
# ::utility::tabget hmm_table OAT/_DATA_/T_MAX
|
||||
|
||||
|
||||
# @brief Create a keyed list from a flat list.
|
||||
# This is useful for inserting a subtable for a new branch.
|
||||
# The branchpath is expressed as a list, ie a/b/c -> {a b c}
|
||||
#
|
||||
# @param flatlist eg {a b c values {1 2 3}}
|
||||
# @return a keyed list, eg a {b {c {values {1 2 3}}}}
|
||||
proc ::utility::tabmktable {flatlist} {
|
||||
if [ catch {
|
||||
if {[llength $flatlist] <= 2} {
|
||||
return $flatlist
|
||||
}
|
||||
set el [lindex $flatlist 0]
|
||||
set table [list $el \$subtable ]
|
||||
foreach el [lrange $flatlist 1 end-2] {
|
||||
set subtable [list $el \$subtable]
|
||||
set table [subst $table]
|
||||
}
|
||||
set subtable [lrange $flatlist end-1 end]
|
||||
set table [subst $table]
|
||||
return $table
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
# If some component of the path doesn't exist then return
|
||||
# a list of indices up to the invalid step. Note if the
|
||||
# first step doesn't exist this returns nothing which is a
|
||||
# valid argument to lset and lindex representing the entire list
|
||||
proc ::utility::tabindices {itable tpath} {
|
||||
if [ catch {
|
||||
upvar $itable table
|
||||
set pathlist [split $tpath /]
|
||||
set subtable $table
|
||||
set indices ""
|
||||
foreach element $pathlist {
|
||||
set datindex [expr 1+[lsearch $subtable $element]]
|
||||
if {$datindex==0} { break }
|
||||
lappend indices $datindex
|
||||
set subtable [lindex $subtable $datindex]
|
||||
}
|
||||
return $indices
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::tabdel {itable tpath} {
|
||||
if [ catch {
|
||||
upvar $itable table
|
||||
set indices [::utility::tabindices table $tpath]
|
||||
if {[llength $indices] != [llength [split $tpath "/"]]} {
|
||||
return
|
||||
}
|
||||
set subtabpos [lrange $indices 0 end-1]
|
||||
set subtable [lindex $table $subtabpos]
|
||||
set datindex [lindex $indices end]
|
||||
set subtable [lreplace $subtable $datindex $datindex]
|
||||
incr datindex -1
|
||||
set subtable [lreplace $subtable $datindex $datindex]
|
||||
lset table $subtabpos $subtable
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::tabget {itable tpath} {
|
||||
upvar $itable table
|
||||
set indices [::utility::tabindices table $tpath]
|
||||
if {[llength $indices] == [llength [split $tpath "/"] ]} {
|
||||
return [lindex $table $indices]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::tabset {itable tpath val} {
|
||||
if [ catch {
|
||||
upvar $itable table
|
||||
set pathlist [split $tpath /]
|
||||
set indices [::utility::tabindices table $tpath]
|
||||
if {[llength $indices] == [llength $pathlist]} {
|
||||
lset table $indices $val
|
||||
} else {
|
||||
set subtable [lindex $table $indices]
|
||||
if {[llength $val] > 1} {
|
||||
set val [list $val]
|
||||
}
|
||||
set plist [ concat [lrange $pathlist [llength $indices] end] $val ]
|
||||
set subtable [concat $subtable [::utility::tabmktable $plist]]
|
||||
lset table $indices $subtable
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
proc ::utility::tabxml {itable tag} {
|
||||
if [ catch {
|
||||
upvar $itable table
|
||||
set subtable [::utility::tabget table $tag]
|
||||
set attributes [::utility::tabget table $tag/_ATTLIST_]
|
||||
set att_text ""
|
||||
foreach {att attval} $attributes {
|
||||
append att_text "\n$att=\"$attval\""
|
||||
}
|
||||
set elements [::utility::tabget table $tag/_ELEMENTS_]
|
||||
foreach el $elements {
|
||||
append content "\n[::utility::tabxml subtable $el]"
|
||||
}
|
||||
append content [::utility::tabget table $tag/_CONTENT_]
|
||||
if {[string trim $att_text] == "" && [string trim $content] == ""} {
|
||||
return
|
||||
} else {
|
||||
return "<$tag $att_text>\n$content\n</$tag>"
|
||||
}
|
||||
} message ] {
|
||||
if {$::errorCode=="NONE"} {return $message}
|
||||
return -code error $message
|
||||
}
|
||||
}
|
||||
|
||||
namespace eval ::utility::macro {}
|
||||
##
|
||||
# @brief Construct a 'getset' kind of macro. A getset macro
|
||||
# will be added automatically to the hdb tree and its return
|
||||
# value will be available for saving.
|
||||
proc ::utility::macro::getset {type name arglist body} {
|
||||
proc ::$name $arglist [subst {
|
||||
$body
|
||||
}]
|
||||
|
||||
publish $name user
|
||||
if {$arglist == ""} {
|
||||
sicslist setatt $name access read_only
|
||||
}
|
||||
sicslist setatt $name privilege manager
|
||||
sicslist setatt $name dtype $type
|
||||
sicslist setatt $name dlen 1
|
||||
sicslist setatt $name data true
|
||||
sicslist setatt $name nxsave true
|
||||
sicslist setatt $name mutable true
|
||||
sicslist setatt $name control true
|
||||
sicslist setatt $name klass @none
|
||||
sicslist setatt $name kind getset
|
||||
sicslist setatt $name savecmd ::nexus::macro::getset_save
|
||||
sicslist setatt $name sdsinfo ::nexus::macro::getset_sdsinfo
|
||||
}
|
||||
namespace import ::utility::*;
|
||||
Publish getinfo spy
|
||||
Publish setpos user
|
||||
|
||||
Reference in New Issue
Block a user