adds a POC preset based count

This commit is contained in:
2025-10-31 13:23:55 +01:00
parent b9e5f40c21
commit 1e853487aa
6 changed files with 460 additions and 62 deletions

View File

@@ -13,7 +13,7 @@ REQUIRED+=asyn
DBDS += src/asynStreamGeneratorDriver.dbd
# DB files to include in the release
TEMPLATES += db/channels.db
TEMPLATES += db/channels.db db/daq_common.db
# HEADERS += src/asynStreamGeneratorDriver.h

View File

@@ -3,9 +3,93 @@
# Macros
# INSTR - Prefix
# NAME - the device name, e.g. EL737
# PORT - Stream Generator Port
# PORT - StreamGenerator Port
# CHANNEL - the number associated with the measurment channel
################################################################################
# Status Variables
# # Trigger a change in status as clearing
# record(bo, "$(INSTR)$(NAME):T$(CHANNEL)")
# {
# field(DESC, "Trigger Clearing Status")
# field(VAL, 1)
# field(OUT, "$(INSTR)$(NAME):S$(CHANNEL) PP")
# }
#
# # Trigger a change in status as value returned to 0
# record(seq, "$(INSTR)$(NAME):O$(CHANNEL)")
# {
# field(DESC, "Trigger Returned to 0 Status")
# field(LNK0, "$(INSTR)$(NAME):S$(CHANNEL) PP")
# field(DO0, 0)
# field(SELM, "Specified")
# field(SELL, "$(INSTR)$(NAME):M$(CHANNEL).VAL")
# }
#
# # Current Status of Channel, i.e. is it ready to count?
# record(bi, "$(INSTR)$(NAME):S$(CHANNEL)")
# {
# field(DESC, "Channel Status")
# field(VAL, 0)
# field(ZNAM, "OK")
# field(ONAM, "CLEARING")
# }
################################################################################
# Count Commands
# # Unfortunately, clearing the channels is somewhat complicated as a result of
# # the addition of more channels over time and minimal changes to the underlying interface
# #
# # Urs Greuter provided the following explanation:
# #
# # bei den Befehlen CC r und HC r ist der Parameter r als bit-Maske zu verstehen:
# #
# # Bit0: Zähler Channel 1
# # Bit2: Zähler Channel 2
# # Bit3: Zähler Channel 3
# # Bit4: Zähler Channel 4
# # Bit5: Zähler Channel Timer
# # Bit6: Zähler Channel 5
# # Bit7: Zähler Channel 6
# # Bit8: Zähler Channel 7
# # Bit9: Zähler Channel 8
# #
# # Beispiele:
# # CC 1 setzt den Zähler des Channels 1 zurück
# # CC 4 setzt den Zähler des Channels 3 zurück
# # CC 5 setzt gleichzeitig die Zähler der Channels 1 und 3 zurück
# # CC 16 ist gleichbedeutend wie CT (Timer zurücksetzen)
# # CC 511 setzt gleichzeitig die Zähler aller Kanäle (auch des Timers) zurück.
#
# record(calc, "$(INSTR)$(NAME):BM$(CHANNEL)")
# {
# field(DESC, "Bit Mask for Channel")
# field(INPA, $(CHANNEL))
# field(CALC, "A > 4 ? 2 ^ A : 2 ^ (A-1)")
# field(PINI, "YES")
# }
#
# record(longout, "$(INSTR)$(NAME):C$(CHANNEL)")
# {
# field(DESC, "Clear the current channel count")
# field(DTYP, "stream")
# field(OMSL, "closed_loop")
# field(DOL, "$(INSTR)$(NAME):BM$(CHANNEL) NPP")
# field(OUT, "@... clearChannel($(INSTR)$(NAME):) $(PORT)")
# field(FLNK, "$(INSTR)$(NAME):T$(CHANNEL)")
# }
#
# record(ao,"$(INSTR)$(NAME):THRESH$(CHANNEL)")
# {
# field(DESC, "Sets min rate for counting to proceed")
# field(OMSL, "supervisory")
# field(OROC, "0")
# field(OUT, "@... setMinRate($(INSTR)$(NAME):, $(CHANNEL)) $(PORT)")
# field(DTYP, "stream")
# }
################################################################################
# Read all monitors values
@@ -15,6 +99,16 @@ record(longin, "$(INSTR)$(NAME):M$(CHANNEL)")
field(EGU, "cts")
field(DTYP, "asynInt32")
field(INP, "@asyn($(PORT),0,$(TIMEOUT=1)) COUNTS$(CHANNEL)")
# This is probably too fast. We could trigger things the same as sinqDAQ to ensure the db is update in the same order
field(SCAN, "I/O Intr")
field(PINI, "YES")
}
# record(ai, "$(INSTR)$(NAME):R$(CHANNEL)")
# {
# field(DESC, "Rate of DAQ CH$(CHANNEL)")
# field(INP, "@... readRate($(INSTR)$(NAME):, $(CHANNEL)) $(PORT)")
# field(DTYP, "stream")
# field(EGU, "cts/sec")
# field(SCAN, "1 second")
# }

212
db/daq_common.db Normal file
View File

@@ -0,0 +1,212 @@
# EPICS Database for streamdevice specific to measurement channels
#
# Macros
# INSTR - Prefix
# NAME - the device name, e.g. EL737
# PORT - StreamGenerator Port
record(longout, "$(INSTR)$(NAME):FULL-RESET")
{
field(DESC, "Reset the DAQ")
field(DTYP, "asynInt32")
field(OUT, "@asyn($(PORT),0,$(TIMEOUT=1)) RESET")
}
################################################################################
# Status Variables
# record(stringin, "$(INSTR)$(NAME):MsgTxt")
# {
# field(DESC, "Unexpected received response")
# field(DTYP, "devDAQStringError")
# field(FLNK, "$(INSTR)$(NAME):INVALID-CONFIG")
# }
record(mbbi, "$(INSTR)$(NAME):STATUS")
{
field(DESC, "DAQ Status")
field(DTYP, "asynInt32")
field(INP, "@asyn($(PORT),0,$(TIMEOUT=1)) STATUS")
field(ZRVL, "0")
field(ZRST, "Idle")
field(ONVL, "1")
field(ONST, "Counting")
field(TWVL, "2")
field(TWST, "Low rate")
field(THVL, "3")
field(THST, "Paused")
# 4 should never happen, if it does it means the DAQ reports undocumented statusbits
field(FRVL, "4")
field(FRST, "INVALID")
# This is probably too fast. We could trigger things the same as sinqDAQ to ensure the db is update in the same order
field(SCAN, "I/O Intr")
field(PINI, "YES")
}
record(longin, "$(INSTR)$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, $(CHANNELS))
field(DISP, 1)
}
# # Trigger a change in status as clearing
# record(bo, "$(INSTR)$(NAME):ETT")
# {
# field(DESC, "Trigger Clearing Status")
# field(VAL, 1)
# field(OUT, "$(INSTR)$(NAME):ETS PP")
# }
#
# # Trigger a change in status as value returned to 0
# record(seq, "$(INSTR)$(NAME):ETO")
# {
# field(DESC, "Trigger Returned to 0 Status")
# field(LNK0, "$(INSTR)$(NAME):ETS PP")
# field(DO0, 0)
# field(SELM, "Specified")
# field(SELL, "$(INSTR)$(NAME):ELAPSED-TIME.VAL")
# }
#
# # Current Status of Channel, i.e. is it ready to count?
# record(bi, "$(INSTR)$(NAME):ETS")
# {
# field(DESC, "Channel Status")
# field(VAL, 0)
# field(ZNAM, "OK")
# field(ONAM, "CLEARING")
# }
################################################################################
# Count Commands
record(ao,"$(INSTR)$(NAME):PRESET-COUNT")
{
field(DESC, "Count until preset reached")
field(DTYP, "asynInt32")
field(OUT, "@asyn($(PORT),0,$(TIMEOUT=1)) P_CNT")
field(VAL, 0)
field(PREC, 2)
}
# record(ao,"$(INSTR)$(NAME):PRESET-TIME")
# {
# field(DESC, "Count for specified time")
# field(DTYP, "stream")
# field(OUT, "@... startWithTimePreset$(CHANNELS)($(INSTR)$(NAME):) $(PORT)")
# field(VAL, 0)
# field(PREC, 2)
# field(EGU, "seconds")
# field(FLNK, "$(INSTR)$(NAME):RAW-STATUS")
# }
#
# record(bo,"$(INSTR)$(NAME):PAUSE")
# {
# field(DESC, "Pause the current count")
# field(DTYP, "stream")
# field(OUT, "@... pauseCount($(INSTR)$(NAME):) $(PORT)")
# field(VAL, "0")
# field(FLNK, "$(INSTR)$(NAME):RAW-STATUS")
# }
#
# record(bo,"$(INSTR)$(NAME):CONTINUE")
# {
# field(DESC, "Continue with a count that was paused")
# field(DTYP, "stream")
# field(OUT, "@... continueCount($(INSTR)$(NAME):) $(PORT)")
# field(VAL, "0")
# field(FLNK, "$(INSTR)$(NAME):RAW-STATUS")
# }
#
# record(longout, "$(INSTR)$(NAME):STOP")
# {
# field(DESC, "Stop the current counting operation")
# field(DTYP, "stream")
# field(OUT, "@... stopCount($(INSTR)$(NAME):) $(PORT)")
# field(FLNK, "$(INSTR)$(NAME):RAW-STATUS")
# }
record(longout, "$(INSTR)$(NAME):MONITOR-CHANNEL")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(DTYP, "asynInt32")
field(OUT, "@asyn($(PORT),0,$(TIMEOUT=1)) MONITOR")
field(DRVL, "1") # Smallest Monitor Channel
field(DRVH, "$(CHANNELS)") # Largest Monitor Channel
}
record(longin, "$(INSTR)$(NAME):MONITOR-CHANNEL_RBV")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(DTYP, "asynInt32")
field(INP, "@asyn($(PORT),0,$(TIMEOUT=1)) MONITOR")
field(SCAN, "I/O Intr")
field(PINI, "YES")
}
# record(calc, "$(INSTR)$(NAME):RATE_MAP")
# {
# field(DESC, "Want a consistent lowrate pv")
# field(INPA, "$(INSTR)$(NAME):RAW-STATUS.B2 NPP")
# field(CALC, "(A=1)?1:0")
# }
#
# record(ao,"$(INSTR)$(NAME):THRESHOLD")
# {
# field(DESC, "Minimum rate for counting to proceed")
# field(VAL, "1") # Default Rate
# # Could perhaps still be improved.
# # It seems to only accept whole counts?
# field(DRVL, "1") # Minimum Rate
# field(DRVH, "100000") # Maximum Rate
# field(OMSL, "supervisory")
# field(OROC, "0")
# field(OUT, "$(INSTR)$(NAME):THRESHOLD-F PP")
# }
#
# record(ai,"$(INSTR)$(NAME):THRESHOLD_RBV")
# {
# field(DESC, "Minimum rate for counting to proceed")
# field(INP, "@... readMinRate($(INSTR)$(NAME):) $(PORT)")
# field(DTYP, "stream")
# field(SCAN, "1 second")
# field(EGU, "cts/sec")
# }
#
# record(longout,"$(INSTR)$(NAME):THRESHOLD-MONITOR")
# {
# field(DESC, "Channel monitored for minimum rate")
# field(VAL, "1") # Monitor
# field(DRVL, "0") # Smallest Threshold Channel (0 is off)
# field(DRVH, "$(CHANNELS)") # Largest Threshold Channel
# field(OUT, "@... setRateMonitor($(INSTR)$(NAME):) $(PORT)")
# field(DTYP, "stream")
# }
#
# record(longin,"$(INSTR)$(NAME):THRESHOLD-MONITOR_RBV")
# {
# field(DESC, "Channel monitored for minimum rate")
# field(INP, "@... readRateMonitor($(INSTR)$(NAME):) $(PORT)")
# field(DTYP, "stream")
# field(SCAN, "1 second")
# field(EGU, "CH")
# }
#
# record(longout, "$(INSTR)$(NAME):CT")
# {
# field(DESC, "Clear the timer")
# field(DTYP, "stream")
# field(OUT, "@... clearTimer($(INSTR)$(NAME):) $(PORT)")
# field(FLNK, "$(INSTR)$(NAME):ETT")
# }
################################################################################
# Read all monitors values
# record(ai,"$(INSTR)$(NAME):ELAPSED-TIME")
# {
# field(DESC, "DAQ Measured Time")
# field(EGU, "sec")
# field(FLNK, "$(INSTR)$(NAME):ETO")
# }

View File

@@ -11,7 +11,12 @@ epicsEnvSet("NAME", "SG")
drvAsynIPPortConfigure("ASYN_IP_PORT", "127.0.0.1:9071:54321 UDP", 0, 0, 0)
asynStreamGenerator("ASYN_SG", "ASYN_IP_PORT", 4)
dbLoadRecords("$(StreamGenerator_DB)daq_common.db", "INSTR=$(INSTR), NAME=$(NAME), PORT=ASYN_SG, CHANNELS=5")
# Detector Count Channel
dbLoadRecords("$(StreamGenerator_DB)channels.db", "INSTR=$(INSTR), NAME=$(NAME), PORT=ASYN_SG, CHANNEL=0")
# Monitor Channels
dbLoadRecords("$(StreamGenerator_DB)channels.db", "INSTR=$(INSTR), NAME=$(NAME), PORT=ASYN_SG, CHANNEL=1")
dbLoadRecords("$(StreamGenerator_DB)channels.db", "INSTR=$(INSTR), NAME=$(NAME), PORT=ASYN_SG, CHANNEL=2")
dbLoadRecords("$(StreamGenerator_DB)channels.db", "INSTR=$(INSTR), NAME=$(NAME), PORT=ASYN_SG, CHANNEL=3")

View File

@@ -90,16 +90,44 @@ asynStreamGeneratorDriver::asynStreamGeneratorDriver(const char *portName,
char pv_name_buffer[100];
P_Counts = new int[this->num_channels];
asynStatus status;
asynStatus status = asynSuccess;
status = (asynStatus)(status | createParam(P_StatusString, asynParamInt32,
&P_Status));
status = (asynStatus)(status | setIntegerParam(P_Status, STATUS_IDLE));
status = (asynStatus)(status |
createParam(P_ResetString, asynParamInt32, &P_Reset));
status = (asynStatus)(status | setIntegerParam(P_Reset, 0));
status = (asynStatus)(status | createParam(P_CountPresetString,
asynParamInt32, &P_CountPreset));
status = (asynStatus)(status | setIntegerParam(P_CountPreset, 0));
status =
(asynStatus)(status | createParam(P_MonitorChannelString,
asynParamInt32, &P_MonitorChannel));
status = (asynStatus)(status | setIntegerParam(P_MonitorChannel, 0));
// Create PVs templated on Channel Number
for (size_t i = 0; i < this->num_channels; ++i) {
memset(pv_name_buffer, 0, 100);
epicsSnprintf(pv_name_buffer, 100, P_CountsString, i);
status = createParam(pv_name_buffer, asynParamInt32, P_Counts + i);
setIntegerParam(P_Counts[i], 0);
status =
(asynStatus)(status | createParam(pv_name_buffer, asynParamInt32,
P_Counts + i));
status = (asynStatus)(status | setIntegerParam(P_Counts[i], 0));
}
if (status) {
printf("%s:%s: failed to create or setup parameters, status=%d\n",
driverName, functionName, status);
exit(1);
}
// Create Events
this->pausedEventId = epicsEventCreate(epicsEventEmpty);
this->monitorProducer = create_kafka_producer();
this->detectorProducer = create_kafka_producer();
@@ -160,6 +188,53 @@ asynStreamGeneratorDriver::~asynStreamGeneratorDriver() {
// epicsStdoutPrintf("Kafka Queue Size %d\n", rd_kafka_outq_len(producer));
}
asynStatus asynStreamGeneratorDriver::writeInt32(asynUser *pasynUser,
epicsInt32 value) {
int function = pasynUser->reason;
asynStatus status = asynSuccess;
const char *paramName;
const char *functionName = "writeInt32";
getParamName(function, &paramName);
// if (status) {
// epicsSnprintf(pasynUser->errorMessage, pasynUser->errorMessageSize,
// "%s:%s: status=%d, function=%d, name=%s, value=%d",
// driverName, functionName, status, function, paramName,
// value);
// return status;
// }
if (function == P_CountPreset) {
setIntegerParam(function, value);
setIntegerParam(P_Status, STATUS_COUNTING);
status = (asynStatus)callParamCallbacks();
epicsEventSignal(this->pausedEventId);
} else if (function == P_Reset) {
// TODO should probably set back everything to defaults
setIntegerParam(P_Status, STATUS_IDLE);
status = (asynStatus)callParamCallbacks();
} else if (function == P_MonitorChannel) {
epicsInt32 currentStatus;
getIntegerParam(this->P_Status, &currentStatus);
if (!currentStatus) {
setIntegerParam(function, value);
status = (asynStatus)callParamCallbacks();
}
} else {
setIntegerParam(function, value);
status = (asynStatus)callParamCallbacks();
}
if (status)
epicsSnprintf(pasynUser->errorMessage, pasynUser->errorMessageSize,
"%s:%s: status=%d, function=%d, name=%s, value=%d",
driverName, functionName, status, function, paramName,
value);
return status;
}
// TODO probably I will have to split this function up, so that the system
// can process the UDP messages in parallel
void asynStreamGeneratorDriver::receiveUDP() {
asynStatus status;
int isConnected;
@@ -170,56 +245,53 @@ void asynStreamGeneratorDriver::receiveUDP() {
int eomReason;
epicsInt32 val;
epicsInt32 currentStatus;
epicsInt32 countPreset = 0;
epicsInt32 presetChannel = 1;
const uint32_t x_pixels = 128;
const uint32_t y_pixels = 128;
const char *functionName = "receiveUDP";
// TODO epics doesn't seem to support uint64, you would need an array of
// uint32. It does support int64 though.. so we start with that
epicsInt32 *counts = new epicsInt32[this->num_channels];
while (true) {
// memset doesn't work with epicsInt32
for (size_t i = 0; i < this->num_channels; ++i) {
counts[i] = 0;
status = getIntegerParam(this->P_Status, &currentStatus);
if (!currentStatus || status) {
epicsEventWait(this->pausedEventId);
getIntegerParam(this->P_CountPreset, &countPreset);
getIntegerParam(this->P_MonitorChannel, &presetChannel);
// memset doesn't work with epicsInt32
for (size_t i = 0; i < this->num_channels; ++i) {
counts[i] = 0;
}
lock();
for (size_t i = 0; i < num_channels; ++i) {
setIntegerParam(P_Counts[i], counts[i]);
}
callParamCallbacks();
unlock();
// Clear the input buffer, in case of stray messages
pasynOctetSyncIO->flush(pasynUDPUser);
}
status = pasynManager->isConnected(pasynUDPUser, &isConnected);
if (status) {
if (!isConnected)
asynPrint(pasynUserSelf, ASYN_TRACE_ERROR,
"%s:%s: error calling pasynManager->isConnected, "
"status=%d, error=%s\n",
driverName, "receiveUDP", status,
pasynUDPUser->errorMessage);
// driverName, functionName, status,
// pasynUserIPPort_->errorMessage);
}
asynPrint(pasynUserSelf, ASYN_TRACEIO_DRIVER,
"%s:%s: isConnected = %d\n", //
driverName, "receiveUDP", isConnected);
"%s:%s: isConnected = %d\n", driverName, functionName,
isConnected);
status = pasynOctetSyncIO->read(pasynUDPUser, buffer, buffer_size,
0, // timeout
&received, &eomReason);
// if (status)
// asynPrint(
// pasynUserSelf, ASYN_TRACE_ERROR,
// "%s:%s: error calling pasynOctetSyncIO->read, status=%d\n",
// driverName, "receiveUDP", status);
// buffer[received] = 0;
if (received) {
// asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: received %f %d
// received\n",
// driverName, "receiveUDP", (double) received /
// 1500., received);
// asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: received
// %d\n",
// driverName, "receiveUDP", received);
UDPHeader *header = (UDPHeader *)buffer;
size_t total_events = (header->BufferLength - 21) / 3;
@@ -230,23 +302,19 @@ void asynStreamGeneratorDriver::receiveUDP() {
// "%s:%s: received packet %d with %d events (%"
// PRIu64
// ")\n",
// driverName, "receiveUDP",
// driverName, functionName,
// header->BufferNumber, total_events,
// header->nanosecs());
for (size_t i = 0; i < total_events; ++i) {
char *event = (buffer + 21 * 2 + i * 6);
if (countPreset && counts[presetChannel] >= countPreset)
break;
if (event[5] & 0x80) { // Monitor Event
MonitorEvent *m_event = (MonitorEvent *)event;
// asynPrint(
// pasynUserSelf, ASYN_TRACE_ERROR,
// "%s:%s: event (%03d) on monitor %d (%" PRIu64
// ")\n", driverName, "receiveUDP", i,
// m_event->DataID, header->nanosecs() +
// (uint64_t)m_event->nanosecs());
counts[m_event->DataID + 1] += 1;
// needs to be freed!!!
@@ -264,25 +332,11 @@ void asynStreamGeneratorDriver::receiveUDP() {
auto nde = new NormalisedDetectorEvent();
nde->TimeStamp =
header->nanosecs() + (uint64_t)d_event->nanosecs();
nde->PixID =
(header->McpdID - 1) * x_pixels * y_pixels +
x_pixels * (uint32_t)d_event->XPosition +
(uint32_t)d_event->YPosition;
nde->PixID = d_event->pixelId(header->McpdID);
this->detectorQueue.push(nde);
}
}
for (size_t i = 0; i < num_channels; ++i) {
getIntegerParam(P_Counts[i], &val);
counts[i] += val;
}
// asynPrint(pasynUserSelf, ASYN_TRACE_ERROR,
// "%s:%s: det: (%d), mon0: (%d), mon1: (%d), mon2: "
// "(%d), mon3: (%d)\n",
// driverName, "receiveUDP", counts[0],
// counts[1], counts[2], counts[3], counts[4]);
lock();
for (size_t i = 0; i < num_channels; ++i) {
setIntegerParam(P_Counts[i], counts[i]);
@@ -292,7 +346,15 @@ void asynStreamGeneratorDriver::receiveUDP() {
} else {
asynPrint(pasynUserSelf, ASYN_TRACE_ERROR,
"%s:%s: invalid UDP packet\n", driverName,
"receiveUDP");
functionName);
}
if (countPreset && counts[presetChannel] >= countPreset) {
lock();
setIntegerParam(P_Status, STATUS_IDLE);
setIntegerParam(P_CountPreset, 0);
callParamCallbacks();
unlock();
}
}

View File

@@ -36,6 +36,12 @@ struct __attribute__((__packed__)) DetectorEvent {
uint16_t Amplitude : 8;
uint16_t Id : 1;
inline uint32_t nanosecs() { return TimeStamp * 100; }
inline uint64_t pixelId(uint32_t mpcdId) {
const uint32_t x_pixels = 128;
const uint32_t y_pixels = 128;
return (mpcdId - 1) * x_pixels * y_pixels +
x_pixels * (uint32_t)this->XPosition + (uint32_t)this->YPosition;
}
};
struct __attribute__((__packed__)) MonitorEvent {
@@ -60,12 +66,24 @@ struct __attribute__((__packed__)) NormalisedDetectorEvent {
uint32_t PixID;
};
/*******************************************************************************
* Status values that should match the definition in db/daq_common.db
*/
#define STATUS_IDLE 0
#define STATUS_COUNTING 1
#define STATUS_LOWRATE 2
#define STATUS_PAUSED 3
/*******************************************************************************
* Parameters for use in DB records
*
* i.e.e drvInfo strings that are used to identify the parameters
*/
#define P_StatusString "STATUS"
#define P_ResetString "RESET"
#define P_CountPresetString "P_CNT"
#define P_MonitorChannelString "MONITOR"
#define P_CountsString "COUNTS%d"
/*******************************************************************************
@@ -77,16 +95,23 @@ class asynStreamGeneratorDriver : public asynPortDriver {
const int numChannels);
virtual ~asynStreamGeneratorDriver();
virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value);
void receiveUDP();
void produceMonitor();
void produceDetector();
protected:
// Parameter Identifying IDs
int P_Status;
int P_Reset;
int P_CountPreset;
int P_MonitorChannel;
int *P_Counts;
private:
asynUser *pasynUDPUser;
epicsEventId pausedEventId;
int num_channels;