StreamDevice: Tips and Tricks

I have many almost identical protocols

You can give arguments to a protocol. In the INP or OUT link, write:

field (OUT, "@protocolfile protocol(arg1,arg2,arg3) bus")

In the protocol, reference arguments as $1 $2 $3 or inside strings as "\$1 \$2 \$3".

moveaxis {out "move\$1 %.6f";}
field (OUT, "@motor.proto moveaxis(X) motor1")

readpressure {out 0x02 0x00 $1; in 0x82 0x00 $1 "%2r";}
field (INP, "@vacuumgauge.proto readpressure(0x84) gauge3")

I have a device that sends unsolicited data

Use I/O Intr processing. The record receives any input and processes only when the input matches.

read {in "new value = %f";}

record (ai, "$(RECORD)") {
  field (DTYP, "stream")
  field (INP, "@$(DEVICETYPE).proto read $(BUS)")
  field (SCAN, "I/O Intr")
}

I have a device that sends multi-line messages

Here is the value:
3.1415

Use as many in commands as you get input lines.

read_value {in "Here is the value:"; in "%f";}

I need to write more than one value in one message

There is more than one solution to this problem. Different approaches have different requirements.

A) All values have the same type and are separated by the same string

Use array records (e.g. waveform, aao).

array_out {separator=", "; out "an array: (%.2f)";}

The format %.2f is repeated for each element of the array. All elements are separated by ", ".
Output will look like this:

an array: (3.14, 17.30, -12.34)

B) We have up to 12 numeric values

Use a calcout record and field references in the format.

write_ABC {out "A=%(A).2f B=%(B).6f C=%(C).0f";}

You must specify a valid expression in the CALC field even if you don't use it.

record (calcout, "$(RECORD)") {
  field (INPA, "$(A_RECORD)")
  field (INPB, "$(B_RECORD)")
  field (INPC, "$(C_RECORD)")
  field (CALC, "0")
  field (DTYP, "stream")
  field (OUT, "@$(DEVICETYPE).proto write_ABC $(BUS)")
}

C) Values are in other records on the same IOC

Use record references in the format.

acquire {out 'ACQUIRE "%(\$1:directory)s/%s",%(\$1:time).3f;';}

You can specify a record name or record.FIELD in parentheses directly after the %. To avoid plain record names in protocol files use protocol arguments like \$1. In the link, specify the record name or just the basename of the other records (device name) in parentheses.

record (stringout, "$(DEVICE):getimage") {
  field (DTYP, "stream")
  field (OUT, "@$(DEVICETYPE).proto acquire($(DEVICE)) $(BUS)")
}

I need to read more than one value from one message

Again, there is more than one solution to this problem.

A) All values have the same type and are separated by the same string

Use array records (e.g. waveform, aai).

array_in {separator=","; in "array = (%f)";}

The format %f is repeated for each element of the array. A "," is expected beween element.
Input may look like this:

array = (3.14, 17.30, -12.34)

B) The message and the values in it can be filtered easily

Use I/O Intr processing and value skipping (%*)

read_A {out "GET A,B"; in "A=%f, B=%*f";}
read_B {in "A=%*f, B=%f";}

record (ai, "$(DEVICE):A") {
  field (DTYP, "stream")
  field (INP, "@$(DEVICETYPE).proto read_A $(BUS)")
  field (SCAN, "1 second")
}
record (ai, "$(DEVICE):B") {
  field (DTYP, "stream")
  field (INP, "@$(DEVICETYPE).proto read_B $(BUS)")
  field (SCAN, "I/O Intr")
}

Record A actively requests values every second. The reply contains values A and B. Record A filters only value A from the input and ignores value B by using the * flag. Nevertheless, a complete syntax check is performed: B must be a valid floating point number. Record B is I/O Intr and gets (a copy of) any input, including input that was directed to record A. If it finds a matching string it ignores value A, reads value B and then processes. Any non-matching input is ignored by record B.

C) Values should be stored in other records on the same IOC

Use record references in the format. To avoid record names in protocol files, use protocol arguments.

read_AB {out "GET A,B"; in "A=%f, B=%(\$1)f";}

record (ai, "$(DEVICE):A") {
  field (DTYP, "stream")
  field (INP, "@$(DEVICETYPE).proto read_AB($(DEVICE):B) $(BUS)")
  field (SCAN, "1 second")
}
record (ai, "$(DEVICE):B") {
}

Whenever record A reads input, it stores the first value in its own VAL field as usual and the second in the VAL field of record B. Because the VAL field of record B has the PP attribute, this automatically processes record B.

I have a device that sends mixed data types: numbers and strings

Use a @mismatch exception handler and record references in the format. To avoid record names in protocol files, use protocol arguments.

Example

When asked "CURRENT?", the device send something like "CURRENT 3.24 A" or a message like "device switched off".

read_current {out "CURRENT?"; in "CURRENT %f A"; @mismatch {in "%(\1)39c";}}

record (ai, "$(DEVICE):readcurrent") {
  field (DTYP, "stream")
  field (INP, "@$(DEVICETYPE).proto read_current($(DEVICE):message) $(BUS)")
}
record (stringin, "$(DEVICE):message") {
}

After processing the readcurrent record, you can see from SEVR/STAT if the read was successful or not. With some more records, you can clean the message record if SEVR is not INVALID.

record (calcout, "$(DEVICE):clean_1") {
  field (INPA, "$(DEVICE):readcurrent.SEVR CP")
  field (CALC, "A!=2")
  field (OOPT, "When Non-zero")
  field (OUT, "$(DEVICE):clean_2.PROC")
}
record (stringout, "$(DEVICE):clean_2") {
  field (VAL, "OK")
  field (OUT, "$(DEVICE):message PP")
}

Dirk Zimoch, 2007