Reorganize where the POD should be placed
The converter now only handles POD from the root DBD object. However there are commands that pull POD out of named sub-objects. This also adds generating tables from menu choices.
This commit is contained in:
committed by
Ralph Lange
parent
f519b63a6f
commit
a05f022e44
@@ -57,6 +57,7 @@ menuGlobal_DBD += menuSimm.dbd
|
||||
DBDINC += $(basename $(menuGlobal_DBD))
|
||||
DBDINC += dbCommon
|
||||
|
||||
HTMLS += $(patsubst %.dbd,%.html,$(menuGlobal_DBD))
|
||||
|
||||
dbCore_SRCS += dbLock.c
|
||||
dbCore_SRCS += dbAccess.c
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
#*************************************************************************
|
||||
# Copyright (c) 2002 The University of Chicago, as Operator of Argonne
|
||||
# Copyright (c) 2012 UChicago Argonne LLC, as Operator of Argonne
|
||||
# National Laboratory.
|
||||
# Copyright (c) 2002 The Regents of the University of California, as
|
||||
# Operator of Los Alamos National Laboratory.
|
||||
# EPICS BASE Versions 3.13.7
|
||||
# and higher are distributed subject to a Software License Agreement found
|
||||
# EPICS BASE is distributed subject to a Software License Agreement found
|
||||
# in file LICENSE that is included with this distribution.
|
||||
#*************************************************************************
|
||||
|
||||
=head1 Menu menuAlarmSevr
|
||||
|
||||
This menu defines the four possible alarm severities that EPICS records can
|
||||
exhibit.
|
||||
|
||||
=menu menuAlarmSevr
|
||||
|
||||
=cut
|
||||
|
||||
menu(menuAlarmSevr) {
|
||||
choice(menuAlarmSevrNO_ALARM,"NO_ALARM")
|
||||
choice(menuAlarmSevrMINOR,"MINOR")
|
||||
|
||||
@@ -6,101 +6,138 @@
|
||||
# EPICS BASE is distributed subject to a Software License Agreement found
|
||||
# in file LICENSE that is included with this distribution.
|
||||
#*************************************************************************
|
||||
recordtype(ai) {
|
||||
|
||||
=head1 Analog Input (ai)
|
||||
=head1 Analog Input Record (ai)
|
||||
|
||||
This record type is normally used to obtain an analog value from
|
||||
a hardware input and convert it to engineering units. The record
|
||||
supports linear and break-point conversion to engineering units,
|
||||
smoothing, alarm limits, alarm filtering, and graphics and control
|
||||
limits.
|
||||
This record type is normally used to obtain an analog value from a hardware
|
||||
input and convert it to engineering units.
|
||||
The record supports linear and break-point conversion to engineering units,
|
||||
smoothing, alarm limits, alarm filtering, and graphics and control limits.
|
||||
|
||||
=head2 Parameter Fields
|
||||
|
||||
The record fields are described below grouped by functionality.
|
||||
The record-specific fields are described below, grouped by functionality.
|
||||
|
||||
=recordtype ai
|
||||
|
||||
=cut
|
||||
|
||||
recordtype(ai) {
|
||||
|
||||
=head3 Input Specification
|
||||
|
||||
These fields control where (if anywhere) the record reads data from
|
||||
when it is processed:
|
||||
These fields control where the record will read data from when it is processed:
|
||||
|
||||
=fields INP, DTYP
|
||||
=fields DTYP, INP
|
||||
|
||||
I<Description?>
|
||||
The DTYP field selects which device support layer should be responsible for
|
||||
providing input data to the record.
|
||||
The ai device support layers provided by EPICS Base are documented in the
|
||||
L<Device Support|devSoft> section.
|
||||
External support modules may provide additional device support for this record
|
||||
type.
|
||||
If not set explicitly, the DTYP value defaults to the first device support that
|
||||
is loaded for the record type, which will usually be the C<Soft Channel> support
|
||||
that comes with Base.
|
||||
|
||||
The INP link field contains a database or channel access link or provides
|
||||
hardware address information that the device support uses to determine where the
|
||||
input data should come from.
|
||||
The format for the INP field value depends on the device support layer that is
|
||||
selected by the DTYP field.
|
||||
See L<Address Specification|...> for a description of the various hardware
|
||||
address formats supported.
|
||||
|
||||
=head3 Units Conversion
|
||||
|
||||
These fields control how the raw input value gets converted into
|
||||
These fields control if and how the raw input value gets converted into
|
||||
engineering units:
|
||||
|
||||
=fields RVAL, ROFF, ASLO, AOFF, LINR, ESLO, EOFF, EGUL, EGUF
|
||||
|
||||
These fields are not used if the device support layer reads its
|
||||
value in engineering units and puts it directly into the VAL field.
|
||||
These fields are not used if the device support layer reads its value in
|
||||
engineering units and puts it directly into the VAL field.
|
||||
This applies to Soft Channel and Async Soft Channel device support, and is also
|
||||
fairly common for GPIB and similar high-level device interfaces.
|
||||
|
||||
If the device support sets the RVAL field, the LINR field controls
|
||||
how this gets converted into engineering units and placed in the
|
||||
VAL field as follows:
|
||||
If the device support sets the RVAL field, the LINR field controls how this gets
|
||||
converted into engineering units and placed in the VAL field as follows:
|
||||
|
||||
=over 4
|
||||
|
||||
=item 1. RVAL is converted to a double and ROFF is added to it
|
||||
=item 1.
|
||||
|
||||
=item 2. If ASLO is non-zero the value is multiplied by ASLO
|
||||
RVAL is converted to a double and ROFF is added to it.
|
||||
|
||||
=item 3. AOFF is added
|
||||
=item 2.
|
||||
|
||||
=item 4. If LINR is C<NO CONVERSION> the units conversion is finished
|
||||
after the above steps
|
||||
If ASLO is non-zero the value is multiplied by ASLO.
|
||||
|
||||
=item 5. If LINR is C<LINEAR> or C<SLOPE>, the value from step 3
|
||||
above is multiplied by ESLO and EOFF is added to complete the units
|
||||
conversion process
|
||||
=item 3.
|
||||
|
||||
=item 6. Any other value for LINR selects a particular breakpoint
|
||||
table to be used on the value from step 3 above
|
||||
AOFF is added.
|
||||
|
||||
=item 4.
|
||||
|
||||
If LINR is C<NO CONVERSION> the units conversion is finished after the above
|
||||
steps.
|
||||
|
||||
=item 5.
|
||||
|
||||
If LINR is C<LINEAR> or C<SLOPE>, the value from step 3 above is multiplied by
|
||||
ESLO and EOFF is added to complete the units conversion process.
|
||||
|
||||
=item 6.
|
||||
|
||||
Any other value for LINR selects a particular breakpoint table to be used on the
|
||||
value from step 3 above.
|
||||
|
||||
=back
|
||||
|
||||
The distinction between the C<LINEAR> and C<SLOPE> settings for the
|
||||
LINR field are in how the conversion parameters are calculated:
|
||||
The distinction between the C<LINEAR> and C<SLOPE> settings for the LINR field
|
||||
are in how the conversion parameters are calculated:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
With C<LINEAR> conversion the user must set EGUL and EGUF to the
|
||||
lowest and highest possible engineering units values respectively
|
||||
that can be converted by the hardware. The device support knows
|
||||
the range of the raw data and calculates ESLO and EOFF from them.
|
||||
With C<LINEAR> conversion the user must set EGUL and EGUF to the lowest and
|
||||
highest possible engineering units values respectively that can be converted by
|
||||
the hardware.
|
||||
The device support knows the range of the raw data and calculates ESLO and EOFF
|
||||
from them.
|
||||
|
||||
=item *
|
||||
C<SLOPE> conversion requires the user to calculate the appropriate
|
||||
scaling and offset factors and put them directly in ESLO and EOFF.
|
||||
C<SLOPE> conversion requires the user to calculate the appropriate scaling and
|
||||
offset factors and put them directly in ESLO and EOFF.
|
||||
|
||||
=back
|
||||
|
||||
=head3 Smoothing Filter
|
||||
|
||||
This filter is usually only used if the device support sets the RVAL
|
||||
field and the Units Conversion process is used. Device support that
|
||||
directly sets the VAL field may implement the filter if desired.
|
||||
This filter is usually only used if the device support sets the RVAL field and
|
||||
the Units Conversion process is used.
|
||||
Device support that directly sets the VAL field may implement the filter if
|
||||
desired.
|
||||
|
||||
The filter is controlled with a single parameter field:
|
||||
|
||||
=fields SMOO
|
||||
|
||||
The SMOO field should be set to a number between 0 and 1. If set to
|
||||
zero the filter is not used (no smoothing), while if set to one the
|
||||
result is infinite smoothing (the VAL field will never change). The
|
||||
calculation performed is:
|
||||
The SMOO field should be set to a number between 0 and 1.
|
||||
If set to zero the filter is not used (no smoothing), while if set to one the
|
||||
result is infinite smoothing (the VAL field will never change).
|
||||
The calculation performed is:
|
||||
|
||||
VAL = VAL * SMOO + (1 - SMOO) * New Data
|
||||
=over 4
|
||||
|
||||
VAL = VAL * SMOO + (1 - SMOO) * New Data
|
||||
|
||||
=back
|
||||
|
||||
where C<New Data> was the result from the Units Conversion above.
|
||||
This implements a first-order infinite impulse response (IIR)
|
||||
digital filter with z-plane pole at SMOO. The equivalent
|
||||
continuous-time filter time constant E<tau> is given by
|
||||
This implements a first-order infinite impulse response (IIR) digital filter
|
||||
with z-plane pole at SMOO.
|
||||
The equivalent continuous-time filter time constant E<tau> is given by
|
||||
|
||||
=over 4
|
||||
|
||||
@@ -113,15 +150,16 @@ where T is the time between record processing.
|
||||
=head3 Undefined Check
|
||||
|
||||
If after applying the smoothing filter the VAL field contains a NaN
|
||||
(Not-a-Number) value, the UDF field is set to indicate that the
|
||||
record value is undefined, which triggers a C<UDF_ALARM> with
|
||||
severity C<INVALID_ALARM>.
|
||||
(Not-a-Number) value, the UDF field is set to a non-zero value, indicating that
|
||||
the record value is undefined, which will trigger a C<UDF_ALARM> with severity
|
||||
C<INVALID_ALARM>.
|
||||
|
||||
=fields UDF
|
||||
|
||||
=head3 Operator Display Parameters
|
||||
|
||||
These parameters are used to present meaningful data to the operator.
|
||||
They do not affect the functioning of the record at all.
|
||||
|
||||
=over 4
|
||||
|
||||
@@ -129,271 +167,464 @@ These parameters are used to present meaningful data to the operator.
|
||||
DESC is a string that is usually used to briefly describe the record.
|
||||
|
||||
=item *
|
||||
EGU is a string of up to 16 characters giving the units that the
|
||||
analog input measures.
|
||||
EGU is a string of up to 16 characters naming the engineering units that the
|
||||
VAL field represents.
|
||||
|
||||
=item *
|
||||
The HOPR and LOPR fields set the upper and lower display limits for
|
||||
the VAL, HIHI, HIGH, LOW, and LOLO fields.
|
||||
|
||||
=item *
|
||||
The PREC field determines the floating point precision with which
|
||||
to display VAL.
|
||||
The PREC field determines the floating point precision (i.e. the number of
|
||||
digits to show after the decimal point) with which to display VAL and the other
|
||||
DOUBLE fields.
|
||||
|
||||
=back
|
||||
|
||||
=fields DESC, EGU, HOPR, LOPR, PREC
|
||||
|
||||
=head3 Alarm Parameters
|
||||
=head3 Alarm Limits
|
||||
|
||||
...
|
||||
The user configures limit alarms by putting numerical values into the HIHI,
|
||||
HIGH, LOW and LOLO fields, and by setting the associated alarm severity in the
|
||||
corresponding HHSV, HSV, LSV and LLSV menu fields.
|
||||
The HYST field controls hysteresis to prevent alarm chattering from an input
|
||||
signal that is close to one of the limits and suffers from significant readout
|
||||
noise.
|
||||
The AFTC field sets the time constant on a low-pass filter that delays the
|
||||
reporting of limit alarms until the signal has been within the alarm range for
|
||||
that number of seconds (the default AFTC value of zero retains the previous
|
||||
behavior).
|
||||
|
||||
=fields HIHI, HIGH, LOW, LOLO, HHSV, HSV, LSV, LLSV, HYST, AFTC
|
||||
=fields HIHI, HIGH, LOW, LOLO, HHSV, HSV, LSV, LLSV, HYST, AFTC, LALM
|
||||
|
||||
=head3 Monitor Parameters
|
||||
|
||||
These parameters are used to determine when to send monitors placed on the VAL
|
||||
field.
|
||||
The monitors are sent when the current value exceeds the last transmitted value
|
||||
by the appropriate deadband.
|
||||
If these fields are set to zero, a monitor will be triggered every time the
|
||||
value changes; if set to -1, a monitor will be sent every time the record is
|
||||
processed.
|
||||
The ADEL field sets the deadband for archive monitors (C<DBE_LOG> events), while
|
||||
the MDEL field controls value monitors (C<DBE_VALUE> events).
|
||||
|
||||
The remaining fields are used by the record at run-time to implement the record
|
||||
monitoring functionality.
|
||||
|
||||
=fields ADEL, MDEL, ALST, MLST, ORAW
|
||||
|
||||
=cut
|
||||
|
||||
include "dbCommon.dbd"
|
||||
field(VAL,DBF_DOUBLE) {
|
||||
prompt("Current EGU Value")
|
||||
promptgroup(GUI_INPUTS)
|
||||
asl(ASL0)
|
||||
pp(TRUE)
|
||||
}
|
||||
field(INP,DBF_INLINK) {
|
||||
prompt("Input Specification")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(PREC,DBF_SHORT) {
|
||||
prompt("Display Precision")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
include "dbCommon.dbd"
|
||||
field(VAL,DBF_DOUBLE) {
|
||||
prompt("Current EGU Value")
|
||||
promptgroup(GUI_INPUTS)
|
||||
asl(ASL0)
|
||||
pp(TRUE)
|
||||
}
|
||||
field(INP,DBF_INLINK) {
|
||||
prompt("Input Specification")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(PREC,DBF_SHORT) {
|
||||
prompt("Display Precision")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(LINR,DBF_MENU) {
|
||||
prompt("Linearization")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
menu(menuConvert)
|
||||
}
|
||||
field(EGUF,DBF_DOUBLE) {
|
||||
prompt("Engineer Units Full")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(EGUL,DBF_DOUBLE) {
|
||||
prompt("Engineer Units Low")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(EGU,DBF_STRING) {
|
||||
prompt("Engineering Units")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
size(16)
|
||||
}
|
||||
field(LINR,DBF_MENU) {
|
||||
prompt("Linearization")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
menu(menuConvert)
|
||||
}
|
||||
field(EGUF,DBF_DOUBLE) {
|
||||
prompt("Engineer Units Full")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(EGUL,DBF_DOUBLE) {
|
||||
prompt("Engineer Units Low")
|
||||
promptgroup(GUI_CONVERT)
|
||||
special(SPC_LINCONV)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(EGU,DBF_STRING) {
|
||||
prompt("Engineering Units")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
size(16)
|
||||
prop(YES)
|
||||
}
|
||||
field(HOPR,DBF_DOUBLE) {
|
||||
prompt("High Operating Range")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(HOPR,DBF_DOUBLE) {
|
||||
prompt("High Operating Range")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(LOPR,DBF_DOUBLE) {
|
||||
prompt("Low Operating Range")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(LOPR,DBF_DOUBLE) {
|
||||
prompt("Low Operating Range")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(AOFF,DBF_DOUBLE) {
|
||||
prompt("Adjustment Offset")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(ASLO,DBF_DOUBLE) {
|
||||
prompt("Adjustment Slope")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
initial("1")
|
||||
}
|
||||
field(SMOO,DBF_DOUBLE) {
|
||||
prompt("Smoothing")
|
||||
promptgroup(GUI_CONVERT)
|
||||
interest(1)
|
||||
}
|
||||
field(HIHI,DBF_DOUBLE) {
|
||||
prompt("Hihi Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(AOFF,DBF_DOUBLE) {
|
||||
prompt("Adjustment Offset")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(ASLO,DBF_DOUBLE) {
|
||||
prompt("Adjustment Slope")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
initial("1")
|
||||
}
|
||||
field(SMOO,DBF_DOUBLE) {
|
||||
prompt("Smoothing")
|
||||
promptgroup(GUI_CONVERT)
|
||||
interest(1)
|
||||
}
|
||||
field(HIHI,DBF_DOUBLE) {
|
||||
prompt("Hihi Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(LOLO,DBF_DOUBLE) {
|
||||
prompt("Lolo Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(LOLO,DBF_DOUBLE) {
|
||||
prompt("Lolo Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(HIGH,DBF_DOUBLE) {
|
||||
prompt("High Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(HIGH,DBF_DOUBLE) {
|
||||
prompt("High Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(LOW,DBF_DOUBLE) {
|
||||
prompt("Low Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(LOW,DBF_DOUBLE) {
|
||||
prompt("Low Alarm Limit")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
}
|
||||
field(HHSV,DBF_MENU) {
|
||||
prompt("Hihi Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
}
|
||||
field(HHSV,DBF_MENU) {
|
||||
prompt("Hihi Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(LLSV,DBF_MENU) {
|
||||
prompt("Lolo Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(LLSV,DBF_MENU) {
|
||||
prompt("Lolo Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(HSV,DBF_MENU) {
|
||||
prompt("High Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(HSV,DBF_MENU) {
|
||||
prompt("High Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(LSV,DBF_MENU) {
|
||||
prompt("Low Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(LSV,DBF_MENU) {
|
||||
prompt("Low Severity")
|
||||
promptgroup(GUI_ALARMS)
|
||||
pp(TRUE)
|
||||
interest(1)
|
||||
prop(YES)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(HYST,DBF_DOUBLE) {
|
||||
prompt("Alarm Deadband")
|
||||
promptgroup(GUI_ALARMS)
|
||||
interest(1)
|
||||
}
|
||||
field(AFTC,DBF_DOUBLE) {
|
||||
prompt("Alarm Filter Time Constant")
|
||||
promptgroup(GUI_ALARMS)
|
||||
interest(1)
|
||||
}
|
||||
field(ADEL,DBF_DOUBLE) {
|
||||
prompt("Archive Deadband")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(MDEL,DBF_DOUBLE) {
|
||||
prompt("Monitor Deadband")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(LALM,DBF_DOUBLE) {
|
||||
prompt("Last Value Alarmed")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(AFVL,DBF_DOUBLE) {
|
||||
prompt("Alarm Filter Value")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(ALST,DBF_DOUBLE) {
|
||||
prompt("Last Value Archived")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(MLST,DBF_DOUBLE) {
|
||||
prompt("Last Val Monitored")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(ESLO,DBF_DOUBLE) {
|
||||
prompt("Raw to EGU Slope")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
initial("1")
|
||||
}
|
||||
field(EOFF,DBF_DOUBLE) {
|
||||
prompt("Raw to EGU Offset")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
}
|
||||
field(ROFF,DBF_LONG) {
|
||||
prompt("Raw Offset, obsolete")
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
}
|
||||
field(PBRK,DBF_NOACCESS) {
|
||||
prompt("Ptrto brkTable")
|
||||
special(SPC_NOMOD)
|
||||
interest(4)
|
||||
extra("void * pbrk")
|
||||
}
|
||||
field(INIT,DBF_SHORT) {
|
||||
prompt("Initialized?")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(LBRK,DBF_SHORT) {
|
||||
prompt("LastBreak Point")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(RVAL,DBF_LONG) {
|
||||
prompt("Current Raw Value")
|
||||
pp(TRUE)
|
||||
}
|
||||
field(ORAW,DBF_LONG) {
|
||||
prompt("Previous Raw Value")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(SIOL,DBF_INLINK) {
|
||||
prompt("Sim Input Specifctn")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(SVAL,DBF_DOUBLE) {
|
||||
prompt("Simulation Value")
|
||||
}
|
||||
field(SIML,DBF_INLINK) {
|
||||
prompt("Sim Mode Location")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(SIMM,DBF_MENU) {
|
||||
prompt("Simulation Mode")
|
||||
interest(1)
|
||||
menu(menuSimm)
|
||||
}
|
||||
field(SIMS,DBF_MENU) {
|
||||
prompt("Sim mode Alarm Svrty")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(2)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
field(HYST,DBF_DOUBLE) {
|
||||
prompt("Alarm Deadband")
|
||||
promptgroup(GUI_ALARMS)
|
||||
interest(1)
|
||||
}
|
||||
field(AFTC,DBF_DOUBLE) {
|
||||
prompt("Alarm Filter Time Constant")
|
||||
promptgroup(GUI_ALARMS)
|
||||
interest(1)
|
||||
}
|
||||
field(ADEL,DBF_DOUBLE) {
|
||||
prompt("Archive Deadband")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(MDEL,DBF_DOUBLE) {
|
||||
prompt("Monitor Deadband")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
interest(1)
|
||||
}
|
||||
field(LALM,DBF_DOUBLE) {
|
||||
prompt("Last Value Alarmed")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(AFVL,DBF_DOUBLE) {
|
||||
prompt("Alarm Filter Value")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(ALST,DBF_DOUBLE) {
|
||||
prompt("Last Value Archived")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(MLST,DBF_DOUBLE) {
|
||||
prompt("Last Val Monitored")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(ESLO,DBF_DOUBLE) {
|
||||
prompt("Raw to EGU Slope")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
initial("1")
|
||||
}
|
||||
field(EOFF,DBF_DOUBLE) {
|
||||
prompt("Raw to EGU Offset")
|
||||
promptgroup(GUI_CONVERT)
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
}
|
||||
field(ROFF,DBF_LONG) {
|
||||
prompt("Raw Offset, obsolete")
|
||||
pp(TRUE)
|
||||
interest(2)
|
||||
}
|
||||
field(PBRK,DBF_NOACCESS) {
|
||||
prompt("Ptrto brkTable")
|
||||
special(SPC_NOMOD)
|
||||
interest(4)
|
||||
extra("void * pbrk")
|
||||
}
|
||||
field(INIT,DBF_SHORT) {
|
||||
prompt("Initialized?")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(LBRK,DBF_SHORT) {
|
||||
prompt("LastBreak Point")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
field(RVAL,DBF_LONG) {
|
||||
prompt("Current Raw Value")
|
||||
pp(TRUE)
|
||||
}
|
||||
field(ORAW,DBF_LONG) {
|
||||
prompt("Previous Raw Value")
|
||||
special(SPC_NOMOD)
|
||||
interest(3)
|
||||
}
|
||||
|
||||
=head3 Simulation Mode
|
||||
|
||||
The record provides several fields to support simulation of absent hardware.
|
||||
If the SIML field is set it is used to read a value into the SIMM field, which
|
||||
controls whether simulation is used or not:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
SIMM must be zero (C<NO>) for the record to request a value from the device
|
||||
support.
|
||||
|
||||
=item *
|
||||
If SIMM is C<YES> and the SIOL link field is set, a simlated value in
|
||||
engineering units is read using the link into the SVAL field, from where it will
|
||||
subsequently be copied into the VAL field.
|
||||
|
||||
=item *
|
||||
If SIMM is C<RAW> the SIOL link is still read into SVAL, but is then truncated
|
||||
and copied into the RVAL field.
|
||||
The L</Units Conversion> process described above is then followed to transform
|
||||
the simulated raw value into engineering units.
|
||||
|
||||
=back
|
||||
|
||||
The SIMS field can be set to give the record an alarm severity while it is in
|
||||
simulation mode.
|
||||
|
||||
=fields SIML, SIMM, SIOL, SVAL, SIMS
|
||||
|
||||
=cut
|
||||
|
||||
field(SIOL,DBF_INLINK) {
|
||||
prompt("Sim. Input Specification")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(SVAL,DBF_DOUBLE) {
|
||||
prompt("Simulation Value")
|
||||
}
|
||||
field(SIML,DBF_INLINK) {
|
||||
prompt("Sim. Mode Location")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(1)
|
||||
}
|
||||
field(SIMM,DBF_MENU) {
|
||||
prompt("Simulation Mode")
|
||||
interest(1)
|
||||
menu(menuSimm)
|
||||
}
|
||||
field(SIMS,DBF_MENU) {
|
||||
prompt("Simulation Mode Severity")
|
||||
promptgroup(GUI_INPUTS)
|
||||
interest(2)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
}
|
||||
|
||||
=head2 Device Support Interface
|
||||
|
||||
The record requires device support to provide an entry table (dset) which
|
||||
defines the following members:
|
||||
|
||||
typedef struct {
|
||||
long number;
|
||||
long (*report)(int level);
|
||||
long (*init)(int after);
|
||||
long (*init_record)(aiRecord *prec);
|
||||
long (*get_ioint_info)(int cmd, aiRecord *prec, IOSCANPVT *piosl);
|
||||
long (*read_ai)(aiRecord *prec);
|
||||
long (*special_linconv)(aiRecord *prec, int after);
|
||||
} aidset;
|
||||
|
||||
The module must set C<number> to at least 6, and provide a pointer to its
|
||||
C<read_ai()> routine; the other function pointers may be C<NULL> if their
|
||||
associated functionality is not required for this support layer.
|
||||
Most device supports also provide an C<init_record()> routine to configure the
|
||||
record instance and connect it to the hardware or driver support layer, and if
|
||||
using the record's L</Units Conversion> features they set C<special_linconv()>
|
||||
as well.
|
||||
The individual routines are described below.
|
||||
|
||||
=head3 Device Support Routines
|
||||
|
||||
=head4 long report(int level)
|
||||
|
||||
This optional routine is called by the IOC command C<dbior> and is passed the
|
||||
report level that was requested by the user.
|
||||
It should print a report on the state of the device support to stdout.
|
||||
The C<level> parameter may be used to output increasingly more detailed
|
||||
information at higher levels, or to select different types of information with
|
||||
different levels.
|
||||
Level zero should print no more than a small summary.
|
||||
|
||||
=head4 long init(int after)
|
||||
|
||||
This optional routine is called twice at IOC initialization time.
|
||||
The first call happens before any of the C<init_record()> calls are made, with
|
||||
the integer parameter C<after> set to 0.
|
||||
The second call happens after all of the C<init_record()> calls have been made,
|
||||
with C<after> set to 1.
|
||||
|
||||
=head4 long init_record(aiRecord *prec)
|
||||
|
||||
This optional routine is called by the record initialization code for each ai
|
||||
record instance that has its DTYP field set to use this device support.
|
||||
It is normally used to check that the INP address is the expected type and that
|
||||
it points to a valid device; to allocate any record-specific buffer space and
|
||||
other memory; and to connect any communication channels needed for the
|
||||
C<read_ai()> routine to work properly.
|
||||
|
||||
If the record type's unit conversion features are used, the C<init_record()>
|
||||
routine should calculate appropriate values for the ESLO and EOFF fields from
|
||||
the EGUL and EGUF field values.
|
||||
This calculation only has to be performed if the record's LINR field is set to
|
||||
C<LINEAR>, but it is not necessary to check that condition first.
|
||||
This same calculation takes place in the C<special_linconv()> routine, so the
|
||||
implementation can usually just call that routine to perform the task.
|
||||
|
||||
=head4 long get_ioint_info(int cmd, aiRecord *prec, IOSCANPVT *piosl)
|
||||
|
||||
This optional routine is called whenever the record's SCAN field is being
|
||||
changed to or from the value C<I/O Intr> to find out which I/O Interrupt Scan
|
||||
list the record should be added to or deleted from.
|
||||
If this routine is not provided, it will not be possible to set the SCAN field
|
||||
to the value C<I/O Intr> at all.
|
||||
|
||||
The C<cmd> parameter is zero when the record is being added to the scan list,
|
||||
and one when it is being removed from the list.
|
||||
The routine must determine which interrupt source the record should be connected
|
||||
to, which it indicates by the scan list that it points the location at C<*piosl>
|
||||
to before returning.
|
||||
It can prevent the SCAN field from being changed at all by returning a non-zero
|
||||
value to its caller.
|
||||
|
||||
In most cases the device support will create the I/O Interrupt Scan lists that
|
||||
it returns for itself, by calling C<void scanIoInit(IOSCANPVT *piosl)> once for
|
||||
each separate interrupt source.
|
||||
That API allocates memory and inializes the list, then passes back a pointer to
|
||||
the new list in the location at C<*piosl>.
|
||||
When the device support receives notification that the interrupt has occurred,
|
||||
it announces that to the IOC by calling C<void scanIoRequest(IOSCANPVT iosl)>
|
||||
which will arrange for the appropriate records to be processed in a suitable
|
||||
thread.
|
||||
The C<scanIoRequest()> routine is safe to call from an interrupt service routine
|
||||
on embedded architectures (vxWorks and RTEMS).
|
||||
|
||||
=head4 long read_ai(aiRecord *prec)
|
||||
|
||||
This essential routine is called whenever the record is processed, and is
|
||||
responsible for performing (or at least initiating) a read operation for the
|
||||
addressed device and (eventually) returning its value to the record.
|
||||
|
||||
... PACT and asynchronous processing ...
|
||||
|
||||
... return value ...
|
||||
|
||||
=head4 long special_linconv(aiRecord *prec, int after)
|
||||
|
||||
This optional routine should be provided if the record type's unit conversion
|
||||
features are used by the device support's C<read_ai()> routine returning a
|
||||
status value of zero.
|
||||
It is called by the record code whenever any of the the fields LINR, EGUL or
|
||||
EGUF are modified and LINR has the value C<LINEAR>.
|
||||
The routine must calculate and set the fields EOFF and ESLO appropriately based
|
||||
on the new values of EGUL and EGUF.
|
||||
|
||||
These calculations can be expressed in terms of the minimum and maximum raw
|
||||
values that the C<read_ai()> routine can put in the RVAL field.
|
||||
When RVAL is set to I<RVAL_max> the VAL field will be set to EGUF, and when RVAL
|
||||
is set to I<RVAL_min> the VAL field will become EGUL.
|
||||
The fomulae to use are:
|
||||
|
||||
=over 4
|
||||
|
||||
EOFF = (I<RVAL_max> * EGUL E<minus> I<RVAL_min> * EGUF) /
|
||||
(I<RVAL_max> E<minus> I<RVAL_min>)
|
||||
|
||||
ESLO = (EGUF E<minus> EGUL) / (I<RVAL_max> E<minus> I<RVAL_min>)
|
||||
|
||||
=back
|
||||
|
||||
Note that the record support sets EOFF to EGUL before calling this routine,
|
||||
which is a very common case (I<RVAL_min> is zero).
|
||||
|
||||
=head3 Extended Device Support
|
||||
|
||||
...
|
||||
|
||||
=cut
|
||||
|
||||
@@ -15,7 +15,7 @@ recordtype(subArray) {
|
||||
special(SPC_DBADDR)
|
||||
pp(TRUE)
|
||||
extra("void * val")
|
||||
#=type See FTVL
|
||||
#=type Set by FTVL
|
||||
#=read Yes
|
||||
#=write Yes
|
||||
}
|
||||
|
||||
@@ -6,11 +6,28 @@
|
||||
# EPICS BASE is distributed subject to a Software License Agreement found
|
||||
# in file LICENSE that is included with this distribution.
|
||||
#*************************************************************************
|
||||
|
||||
=head1 Waveform Record (waveform)
|
||||
|
||||
...
|
||||
|
||||
=recordtype waveform
|
||||
|
||||
...
|
||||
|
||||
=cut
|
||||
|
||||
menu(waveformPOST) {
|
||||
choice(waveformPOST_Always,"Always")
|
||||
choice(waveformPOST_OnChange,"On Change")
|
||||
choice(waveformPOST_Always,"Always")
|
||||
choice(waveformPOST_OnChange,"On Change")
|
||||
}
|
||||
|
||||
recordtype(waveform) {
|
||||
|
||||
=fields VAL, FTVL, MPST, APST
|
||||
|
||||
=cut
|
||||
|
||||
include "dbCommon.dbd"
|
||||
field(VAL,DBF_NOACCESS) {
|
||||
prompt("Value")
|
||||
@@ -18,7 +35,7 @@ recordtype(waveform) {
|
||||
special(SPC_DBADDR)
|
||||
pp(TRUE)
|
||||
extra("void * val")
|
||||
#=type See FTVL
|
||||
#=type Set by FTVL
|
||||
#=read Yes
|
||||
#=write Yes
|
||||
}
|
||||
@@ -107,6 +124,17 @@ recordtype(waveform) {
|
||||
interest(2)
|
||||
menu(menuAlarmSevr)
|
||||
}
|
||||
|
||||
=head3 Menu waveformPOST
|
||||
|
||||
...
|
||||
|
||||
=menu waveformPOST
|
||||
|
||||
...
|
||||
|
||||
=cut
|
||||
|
||||
field(MPST,DBF_MENU) {
|
||||
prompt("Post Value Monitors")
|
||||
promptgroup(GUI_DISPLAY)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/perl
|
||||
#*************************************************************************
|
||||
# Copyright (c) 2010 UChicago Argonne LLC, as Operator of Argonne
|
||||
# Copyright (c) 2012 UChicago Argonne LLC, as Operator of Argonne
|
||||
# National Laboratory.
|
||||
# EPICS BASE is distributed subject to a Software License Agreement found
|
||||
# in file LICENSE that is included with this distribution.
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
# $Id$
|
||||
|
||||
use strict;
|
||||
|
||||
use FindBin qw($Bin);
|
||||
use lib "$Bin/../../lib/perl";
|
||||
|
||||
@@ -19,6 +21,8 @@ use EPICS::Readfile;
|
||||
use Pod::Simple::HTML;
|
||||
|
||||
my $tool = 'dbdToHtml';
|
||||
|
||||
use vars qw($opt_D @opt_I $opt_o);
|
||||
getopts('DI@o:') or
|
||||
die "Usage: $tool [-D] [-I dir] [-o file.html] file.dbd\n";
|
||||
|
||||
@@ -44,39 +48,31 @@ if ($opt_D) { # Output dependencies only
|
||||
exit 0;
|
||||
}
|
||||
|
||||
open my $out, '>', $opt_o or die "Can't create $opt_o: $!\n";
|
||||
open my $out, '>', $opt_o or
|
||||
die "Can't create $opt_o: $!\n";
|
||||
|
||||
# Grab the Pod text from the root DBD object
|
||||
my @pod = $dbd->pod;
|
||||
|
||||
my $rtypes = $dbd->recordtypes;
|
||||
|
||||
# Append the processed Pod text from any record types defined
|
||||
while (my ($rn, $rtyp) = each %{$rtypes}) {
|
||||
foreach my $_ ($rtyp->pod) {
|
||||
# Handle our 'fields' Pod directive
|
||||
if (m/^=fields (.*)/) {
|
||||
my @names = split /\s*,\s*/, $1;
|
||||
# Look up every named field
|
||||
my @fields = map {
|
||||
my $field = $rtyp->field($_);
|
||||
print STDERR "Unknown field name '$_' in $infile POD\n" unless $field;
|
||||
$field;
|
||||
} @names;
|
||||
my $html;
|
||||
# Generate a HTML table row for each field
|
||||
foreach $field (@fields) {
|
||||
$html .= $field->htmlTableRow if $field;
|
||||
}
|
||||
# Add the complete table
|
||||
push @pod, podTable($html);
|
||||
}
|
||||
else {
|
||||
# Add other Pod text
|
||||
push @pod, $_;
|
||||
}
|
||||
# Parse the Pod text from the root DBD object
|
||||
my @pod = map {
|
||||
# Handle a 'recordtype' Pod directive
|
||||
if (m/^ =recordtype \s+ (.*)/x) {
|
||||
my $rn = $1;
|
||||
my $rtyp = $dbd->recordtype($rn);
|
||||
die "Unknown recordtype '$rn' in $infile POD directive\n"
|
||||
unless $rtyp;
|
||||
rtypeToPod($rtyp, $dbd);
|
||||
}
|
||||
}
|
||||
# Handle a 'menu' Pod directive
|
||||
elsif (m/^ =menu \s+ (.*)/x) {
|
||||
my $mn = $1;
|
||||
my $menu = $dbd->menu($mn);
|
||||
die "Unknown menu '$mn' in $infile POD directive\n"
|
||||
unless $menu;
|
||||
menuToPod($menu);
|
||||
}
|
||||
else {
|
||||
$_;
|
||||
}
|
||||
} $dbd->pod;
|
||||
|
||||
my $podHtml = Pod::Simple::HTML->new();
|
||||
$podHtml->html_css('style.css');
|
||||
@@ -90,18 +86,70 @@ print $out $html;
|
||||
close $out;
|
||||
|
||||
|
||||
sub podTable {
|
||||
my $content = shift;
|
||||
return ("=begin html\n", "\n",
|
||||
sub menuToPod {
|
||||
my ($menu) = @_;
|
||||
my $index = 0;
|
||||
return "=begin html\n", "\n",
|
||||
"<blockquote><table border =\"1\"><tr>\n",
|
||||
"<th>Field</th><th>Summary</th><th>Type</th><th>DCT</th>",
|
||||
"<th>Default</th><th>Read</th><th>Write</th><th>CA PP</th></tr>\n",
|
||||
$content, "</table></blockquote>\n",
|
||||
"\n", "=end html\n");
|
||||
"<th>Index</th><th>Identifier</th><th>Choice String</th>\n",
|
||||
"</tr>\n",
|
||||
map({choiceTableRow($_, $index++)} $menu->choices),
|
||||
"</table></blockquote>\n",
|
||||
"\n", "=end html\n";
|
||||
}
|
||||
|
||||
sub DBD::Recfield::htmlTableRow {
|
||||
my $fld = shift;
|
||||
sub choiceTableRow {
|
||||
my ($ch, $index) = @_;
|
||||
my ($id, $name) = @{$ch};
|
||||
return '<tr><td class="cell">',
|
||||
$index,
|
||||
'</td><td class="cell">',
|
||||
$id,
|
||||
'</td><td class="cell">',
|
||||
$name,
|
||||
"</td></tr>\n";
|
||||
}
|
||||
|
||||
sub rtypeToPod {
|
||||
my ($rtyp, $dbd) = @_;
|
||||
return map {
|
||||
# Handle a 'fields' Pod directive
|
||||
if (m/^ =fields \s+ (.*)/x) {
|
||||
my @names = split /\s*,\s*/, $1;
|
||||
# Look up the named fields
|
||||
my @fields = map {
|
||||
my $field = $rtyp->field($_);
|
||||
die "Unknown field name '$_' in $infile POD\n"
|
||||
unless $field;
|
||||
$field;
|
||||
} @names;
|
||||
# Generate Pod for the table
|
||||
"=begin html\n", "\n",
|
||||
"<blockquote><table border =\"1\"><tr>\n",
|
||||
"<th>Field</th><th>Summary</th><th>Type</th><th>DCT</th>",
|
||||
"<th>Default</th><th>Read</th><th>Write</th><th>CA PP</th>",
|
||||
"</tr>\n",
|
||||
map({fieldTableRow($_, $dbd)} @fields),
|
||||
"</table></blockquote>\n",
|
||||
"\n", "=end html\n";
|
||||
}
|
||||
# Handle a 'menu' Pod directive
|
||||
elsif (m/^ =menu \s+ (.*)/x) {
|
||||
my $mn = $1;
|
||||
my $menu = $dbd->menu($mn);
|
||||
die "Unknown menu '$mn' in $infile POD directive\n"
|
||||
unless $menu;
|
||||
menuToPod($menu);
|
||||
}
|
||||
else {
|
||||
# Raw text line
|
||||
$_;
|
||||
}
|
||||
} $rtyp->pod;
|
||||
}
|
||||
|
||||
sub fieldTableRow {
|
||||
my ($fld, $dbd) = @_;
|
||||
my $html = '<tr><td class="cell">';
|
||||
$html .= $fld->name;
|
||||
$html .= '</td><td class="cell">';
|
||||
@@ -111,8 +159,12 @@ sub DBD::Recfield::htmlTableRow {
|
||||
$html .= $type;
|
||||
$html .= ' [' . $fld->attribute('size') . ']'
|
||||
if $type eq 'STRING';
|
||||
$html .= ' (' . $fld->attribute('menu') . ')'
|
||||
if $type eq 'MENU';
|
||||
if ($type eq 'MENU') {
|
||||
my $mn = $fld->attribute('menu');
|
||||
my $menu = $dbd->menu($mn);
|
||||
my $url = $menu ? "#Menu_$mn" : "${mn}.html";
|
||||
$html .= " (<a href='$url'>$mn</a>)";
|
||||
}
|
||||
$html .= '</td><td class="cell">';
|
||||
$html .= $fld->attribute('promptgroup') ? 'Yes' : 'No';
|
||||
$html .= '</td><td class="cell">';
|
||||
|
||||
Reference in New Issue
Block a user