From 0e39c8f3fda99b25317aee93685fd704841ed4b6 Mon Sep 17 00:00:00 2001 From: Jan Jug Date: Thu, 12 Feb 2026 14:03:28 +0100 Subject: [PATCH 1/2] Add support for multiple cameras This commit enables the driver to support multiple cameras simultaneously by accepting different interface and camera IDs. Key changes: - implemented GenTL singleton so that GenTL is initialized only once per IOC, which is required for multi-camera operation - added thread-safe reference counting for GenTL lifecycle management - added ADEuresysConfig2() function that accepts interfaceIndex and deviceIndex parameters for explicit card and camera selection - kept the original function unchanged for backward compatibility - included a multi-camera configuration example with 3 cameras --- EuresysApp/src/ADEuresys.cpp | 202 +++++++++++++++--- EuresysApp/src/ADEuresys.h | 15 +- .../iocAdimec/st_multi_camera.cmd.example | 100 +++++++++ 3 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 iocs/EuresysIOC/iocBoot/iocAdimec/st_multi_camera.cmd.example diff --git a/EuresysApp/src/ADEuresys.cpp b/EuresysApp/src/ADEuresys.cpp index e9c90ed..c31c3db 100755 --- a/EuresysApp/src/ADEuresys.cpp +++ b/EuresysApp/src/ADEuresys.cpp @@ -6,6 +6,10 @@ * Author: Mark Rivers * University of Chicago * + * Contributor: Jan Jug + * Cosylab d.d. + * jan.jug@cosylab.com + * * Created: March 6, 2024 * */ @@ -23,6 +27,7 @@ #include #include #include +#include #include #include @@ -38,6 +43,11 @@ using namespace Euresys; static const char *driverName = "ADEuresys"; +/* GenTL system management singleton */ +static EGenTL *pGenTL = NULL; +static int genTLRefCount = 0; +static epicsMutex *pGenTLMutex = new epicsMutex(); + typedef enum { TimeStampCamera, TimeStampEPICS @@ -48,33 +58,57 @@ typedef enum { UniqueIdDriver } ESUniqueId_t; -/** Configuration function to configure one camera. +/** Original configuration function for backward compatibility. + * + * This function maintains backward compatibility with the old API. The cameraId parameter + * is ignored (it was never used in the original implementation). This always connects to + * interface 0, device 0, which matches the original behavior. + * \param[in] portName asyn port name to assign to the camera. + * \param[in] cameraId Legacy parameter (ignored - kept for backward compatibility). + * \param[in] numEGBuffers The number of buffers to allocate in EGrabber. + * If set to 0 or omitted the default of 100 will be used. + * \param[in] maxMemory Maximum memory (in bytes) that this driver is allowed to allocate. 0=unlimited. + * \param[in] priority The EPICS thread priority for this driver. 0=use asyn default. + * \param[in] stackSize The size of the stack for the EPICS port thread. 0=use asyn default. + */ +extern "C" int ADEuresysConfig(const char *portName, const char* cameraId, + int numEGBuffers, size_t maxMemory, int priority, int stackSize) +{ + new ADEuresys(portName, cameraId, numEGBuffers, maxMemory, priority, stackSize); + return asynSuccess; +} + +/** Configuration function to configure one camera with explicit interface and device indices. * * This function need to be called once for each camera to be used by the IOC. A call to this * function instantiates one object from the ADEuresys class. * \param[in] portName asyn port name to assign to the camera. - * \param[in] cameraId A string identifying the camera to control. + * \param[in] interfaceIndex The GenTL interface index (typically the board/card index). + * \param[in] deviceIndex The GenTL device index (camera index on the specified interface). * \param[in] numEGBuffers The number of buffers to allocate in EGrabber. * If set to 0 or omitted the default of 100 will be used. * \param[in] maxMemory Maximum memory (in bytes) that this driver is allowed to allocate. 0=unlimited. * \param[in] priority The EPICS thread priority for this driver. 0=use asyn default. * \param[in] stackSize The size of the stack for the EPICS port thread. 0=use asyn default. */ -extern "C" int ADEuresysConfig(const char *portName, const char* cameraId, int numEGBuffers, - size_t maxMemory, int priority, int stackSize) +extern "C" int ADEuresysConfig2(const char *portName, int interfaceIndex, int deviceIndex, + int numEGBuffers, size_t maxMemory, int priority, int stackSize) { - new ADEuresys(portName, cameraId, numEGBuffers, maxMemory, priority, stackSize); + new ADEuresys(portName, interfaceIndex, deviceIndex, numEGBuffers, + maxMemory, priority, stackSize); return asynSuccess; } class myGrabber : public EGRABBER_CALLBACK { public: - myGrabber(EGenTL *gentl, class ADEuresys *pADEuresys) : EGRABBER_CALLBACK(*gentl) { + myGrabber(EGenTL *gentl, int interfaceIndex, int deviceIndex, + class ADEuresys *pADEuresys) + : EGRABBER_CALLBACK(*gentl, interfaceIndex, deviceIndex) { pADEuresys_ = pADEuresys; enableEvent(); } private: - class ADEuresys *pADEuresys_; + class ADEuresys *pADEuresys_; virtual void onNewBufferEvent(const NewBufferData &data) { ScopedBuffer buf(*this, data); pADEuresys_->processFrame(buf); @@ -88,28 +122,116 @@ static void c_shutdown(void *arg) p->shutdown(); } -/** Constructor for the ADEuresys class +/** Initialize the GenTL singleton. + * This method creates the GenTL system object on first call and increments reference count. + */ +void ADEuresys::initGenTL() +{ + static const char *functionName = "initGenTL"; + + pGenTLMutex->lock(); + if (pGenTL == NULL) { + try { + pGenTL = new EGenTL(); + errlogPrintf("%s::%s: Created GenTL system singleton\n", + driverName, functionName); + } + catch (std::exception &e) { + errlogPrintf("%s::%s ERROR: Failed to create GenTL system: %s\n", + driverName, functionName, e.what()); + pGenTLMutex->unlock(); + throw; + } + } + genTLRefCount++; + errlogPrintf("%s::%s: GenTL reference count = %d\n", + driverName, functionName, genTLRefCount); + pGenTLMutex->unlock(); +} + +/** Cleanup the GenTL singleton. + * This method decrements reference count and deletes the GenTL system object when count reaches zero. + */ +void ADEuresys::cleanupGenTL() +{ + static const char *functionName = "cleanupGenTL"; + + pGenTLMutex->lock(); + genTLRefCount--; + errlogPrintf("%s::%s: GenTL reference count = %d\n", + driverName, functionName, genTLRefCount); + + if (genTLRefCount <= 0 && pGenTL != NULL) { + delete pGenTL; + pGenTL = NULL; + errlogPrintf("%s::%s: Deleted GenTL system singleton\n", + driverName, functionName); + } + pGenTLMutex->unlock(); +} + +/** Legacy constructor for backward compatibility. + * Ignores cameraId and uses interface 0, device 0 (matching original behavior). * \param[in] portName asyn port name to assign to the camera. - * \param[in] cameraId A string identifying the camera to control. + * \param[in] cameraId Legacy parameter (ignored). * \param[in] numEGBuffers The number of buffers to allocate in EGrabber. - * If set to 0 or omitted the default of 100 will be used. * \param[in] maxMemory Maximum memory (in bytes) that this driver is allowed to allocate. 0=unlimited. * \param[in] priority The EPICS thread priority for this driver. 0=use asyn default. * \param[in] stackSize The size of the stack for the EPICS port thread. 0=use asyn default. */ ADEuresys::ADEuresys(const char *portName, const char* cameraId, int numEGBuffers, - size_t maxMemory, int priority, int stackSize ) + size_t maxMemory, int priority, int stackSize) + : ADEuresys(portName, 0, 0, numEGBuffers, maxMemory, priority, stackSize) +{ + // Delegate to new constructor with interface 0, device 0 + // Note: cameraId parameter is ignored as it was never used in the original implementation +} + +/** Constructor for the ADEuresys class with explicit interface and device indices. + * \param[in] portName asyn port name to assign to the camera. + * \param[in] interfaceIndex The GenTL interface index (typically the board/card index). + * \param[in] deviceIndex The GenTL device index (camera index on the specified interface). + * \param[in] numEGBuffers The number of buffers to allocate in EGrabber. + * If set to 0 or omitted the default of 100 will be used. + * \param[in] maxMemory Maximum memory (in bytes) that this driver is allowed to allocate. 0=unlimited. + * \param[in] priority The EPICS thread priority for this driver. 0=use asyn default. + * \param[in] stackSize The size of the stack for the EPICS port thread. 0=use asyn default. + */ +ADEuresys::ADEuresys(const char *portName, int interfaceIndex, int deviceIndex, + int numEGBuffers, size_t maxMemory, int priority, int stackSize ) : ADGenICam(portName, maxMemory, priority, stackSize), numEGBuffers_(numEGBuffers), exiting_(0), uniqueId_(0) { - //static const char *functionName = "ADEuresys"; - + static const char *functionName = "ADEuresys"; + //pasynTrace->setTraceMask(pasynUserSelf, ASYN_TRACE_ERROR | ASYN_TRACE_WARNING | ASYN_TRACEIO_DRIVER); - + if (numEGBuffers_ == 0) numEGBuffers_ = 100; - EGenTL *gentl = new EGenTL; - mGrabber_ = new myGrabber(gentl, this); - mGrabber_->reallocBuffers(numEGBuffers); + + // Initialize the singleton GenTL system (increments ref count) + initGenTL(); + + try { + // Create EGrabber with specific interface and device indices using singleton + mGrabber_ = new myGrabber(pGenTL, interfaceIndex, deviceIndex, this); + mGrabber_->reallocBuffers(numEGBuffers); + + asynPrint(pasynUserSelf, ASYN_TRACE_FLOW, + "%s::%s connected to interface %d, device %d\n", + driverName, functionName, interfaceIndex, deviceIndex); + } + catch (std::exception &e) { + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s::%s ERROR creating EGrabber for interface %d, device %d: %s\n", + driverName, functionName, interfaceIndex, deviceIndex, e.what()); + // Clean up mGrabber_ if it was created but reallocBuffers failed + if (mGrabber_) { + delete mGrabber_; + mGrabber_ = NULL; + } + cleanupGenTL(); // Decrement ref count since we failed + throw; + } createParam(ESTimeStampModeString, asynParamInt32, &ESTimeStampMode); createParam(ESUniqueIdModeString, asynParamInt32, &ESUniqueIdMode); @@ -151,12 +273,17 @@ EGRABBER_CALLBACK* ADEuresys::getGrabber() { void ADEuresys::shutdown(void) { - //static const char *functionName = "shutdown"; lock(); exiting_ = 1; stopCapture(); - delete mGrabber_; + if (mGrabber_) { + delete mGrabber_; + mGrabber_ = NULL; + } unlock(); + + // Decrement GenTL reference count and cleanup if last instance + cleanupGenTL(); } GenICamFeature *ADEuresys::createFeature(GenICamFeatureSet *set, @@ -439,6 +566,7 @@ void ADEuresys::report(FILE *fp, int details) return; } +// Legacy iocsh configuration for backward compatibility static const iocshArg configArg0 = {"Port name", iocshArgString}; static const iocshArg configArg1 = {"Camera ID", iocshArgString}; static const iocshArg configArg2 = {"# EGrabber buffers", iocshArgInt}; @@ -446,22 +574,44 @@ static const iocshArg configArg3 = {"maxMemory", iocshArgInt}; static const iocshArg configArg4 = {"priority", iocshArgInt}; static const iocshArg configArg5 = {"stackSize", iocshArgInt}; static const iocshArg * const configArgs[] = {&configArg0, - &configArg1, - &configArg2, - &configArg3, - &configArg4, - &configArg5}; + &configArg1, + &configArg2, + &configArg3, + &configArg4, + &configArg5}; static const iocshFuncDef configADEuresys = {"ADEuresysConfig", 6, configArgs}; static void configCallFunc(const iocshArgBuf *args) { - ADEuresysConfig(args[0].sval, args[1].sval, args[2].ival, args[3].ival, - args[4].ival, args[5].ival); + ADEuresysConfig(args[0].sval, args[1].sval, args[2].ival, + args[3].ival, args[4].ival, args[5].ival); } +// New iocsh configuration with explicit interface and device indices +static const iocshArg config2Arg0 = {"Port name", iocshArgString}; +static const iocshArg config2Arg1 = {"Interface index", iocshArgInt}; +static const iocshArg config2Arg2 = {"Device index", iocshArgInt}; +static const iocshArg config2Arg3 = {"# EGrabber buffers", iocshArgInt}; +static const iocshArg config2Arg4 = {"maxMemory", iocshArgInt}; +static const iocshArg config2Arg5 = {"priority", iocshArgInt}; +static const iocshArg config2Arg6 = {"stackSize", iocshArgInt}; +static const iocshArg * const config2Args[] = {&config2Arg0, + &config2Arg1, + &config2Arg2, + &config2Arg3, + &config2Arg4, + &config2Arg5, + &config2Arg6}; +static const iocshFuncDef configADEuresys2 = {"ADEuresysConfig2", 7, config2Args}; +static void configCallFunc2(const iocshArgBuf *args) +{ + ADEuresysConfig2(args[0].sval, args[1].ival, args[2].ival, args[3].ival, + args[4].ival, args[5].ival, args[6].ival); +} static void ADEuresysRegister(void) { iocshRegister(&configADEuresys, configCallFunc); + iocshRegister(&configADEuresys2, configCallFunc2); } extern "C" { diff --git a/EuresysApp/src/ADEuresys.h b/EuresysApp/src/ADEuresys.h index 841d508..7b219cb 100644 --- a/EuresysApp/src/ADEuresys.h +++ b/EuresysApp/src/ADEuresys.h @@ -2,6 +2,7 @@ #define ADEURESYS_H #include +#include #include #include @@ -29,21 +30,31 @@ typedef EGrabber EGRABBER_CALLBACK; class ADEuresys : public ADGenICam { public: + // Legacy constructor for backward compatibility - ignores cameraId and uses interface 0, device 0 ADEuresys(const char *portName, const char* cameraId, int numESBuffers, size_t maxMemory, int priority, int stackSize); + // New constructor with explicit interface and device indices + ADEuresys(const char *portName, int interfaceIndex, int deviceIndex, + int numESBuffers, size_t maxMemory, int priority, int stackSize); + // virtual methods to override from ADGenICam void report(FILE *fp, int details); virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value); - virtual GenICamFeature *createFeature(GenICamFeatureSet *set, + virtual GenICamFeature *createFeature(GenICamFeatureSet *set, std::string const & asynName, asynParamType asynType, int asynIndex, std::string const & featureName, GCFeatureType_t featureType); - + void processFrame(ScopedBuffer &buf); EGRABBER_CALLBACK *getGrabber(); void shutdown(); private: + /* Static methods for GenTL singleton management */ + static void initGenTL(); + static void cleanupGenTL(); + + /* parameters */ int ESTimeStampMode; #define FIRST_ES_PARAM ESTimeStampMode int ESUniqueIdMode; diff --git a/iocs/EuresysIOC/iocBoot/iocAdimec/st_multi_camera.cmd.example b/iocs/EuresysIOC/iocBoot/iocAdimec/st_multi_camera.cmd.example new file mode 100644 index 0000000..95b5652 --- /dev/null +++ b/iocs/EuresysIOC/iocBoot/iocAdimec/st_multi_camera.cmd.example @@ -0,0 +1,100 @@ +< envPaths +errlogInit(20000) + +dbLoadDatabase("$(TOP)/dbd/EuresysApp.dbd") +EuresysApp_registerRecordDeviceDriver(pdbbase) + +# Multi-camera configuration example +# This example shows how to configure 3 cameras on a single Euresys card + +# Common settings +epicsEnvSet("PREFIX", "13ES1:") +epicsEnvSet("QSIZE", "2000") +epicsEnvSet("NCHANS", "2048") +epicsEnvSet("CBUFFS", "500") +epicsEnvSet("XSIZE", "4096") +epicsEnvSet("YSIZE", "3072") +epicsEnvSet("NELEMENTS", "12582912") # Enough for 4096x3072 mono image + +# Database file paths +epicsEnvSet("EPICS_DB_INCLUDE_PATH", "$(ADCORE)/db;$(ADGENICAM)/db;$(ADEURESYS)/db") +epicsEnvSet("GENICAM_DB_FILE", "$(ADGENICAM)/db/Adimec_Q12A180CXP_1_1_5.template") + +# Interface 0 = first Euresys card in the system +epicsEnvSet("INTERFACE_ID", "0") + +#============================================================================== +# Camera 1 configuration (Interface 0, Device 0) +#============================================================================== +epicsEnvSet("PORT1", "ES1_CAM1") + +# ADEuresysConfig2(portName, interfaceIndex, deviceIndex, numBuffers, maxMemory, priority, stackSize) +ADEuresysConfig2("$(PORT1)", $(INTERFACE_ID), 0, 100, 0, 0, 0) +asynSetTraceIOMask($(PORT1), 0, ESCAPE) + +# Load database records for Camera 1 +dbLoadRecords("$(ADEURESYS)/db/Euresys.template", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") +dbLoadRecords("$(GENICAM_DB_FILE)", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDevice.template", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDataStream.template", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLInterface.template", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLSystem.template", "P=$(PREFIX),R=cam1:,PORT=$(PORT1)") + +# Camera 1 plugins +NDStdArraysConfigure("Image1", 5, 0, "$(PORT1)", 0, 0) +dbLoadRecords("$(ADCORE)/db/NDStdArrays.template", "P=$(PREFIX),R=image1:,PORT=Image1,ADDR=0,TIMEOUT=1,NDARRAY_PORT=$(PORT1),TYPE=Int8,FTVL=CHAR,NELEMENTS=$(NELEMENTS)") + +#============================================================================== +# Camera 2 configuration (Interface 0, Device 1) +#============================================================================== +epicsEnvSet("PORT2", "ES1_CAM2") + +# ADEuresysConfig2(portName, interfaceIndex, deviceIndex, numBuffers, maxMemory, priority, stackSize) +ADEuresysConfig2("$(PORT2)", $(INTERFACE_ID), 1, 100, 0, 0, 0) +asynSetTraceIOMask($(PORT2), 0, ESCAPE) + +# Load database records for Camera 2 +dbLoadRecords("$(ADEURESYS)/db/Euresys.template", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") +dbLoadRecords("$(GENICAM_DB_FILE)", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDevice.template", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDataStream.template", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLInterface.template", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLSystem.template", "P=$(PREFIX),R=cam2:,PORT=$(PORT2)") + +# Camera 2 plugins +NDStdArraysConfigure("Image2", 5, 0, "$(PORT2)", 0, 0) +dbLoadRecords("$(ADCORE)/db/NDStdArrays.template", "P=$(PREFIX),R=image2:,PORT=Image2,ADDR=0,TIMEOUT=1,NDARRAY_PORT=$(PORT2),TYPE=Int8,FTVL=CHAR,NELEMENTS=$(NELEMENTS)") + +#============================================================================== +# Camera 3 configuration (Interface 0, Device 2) +#============================================================================== +epicsEnvSet("PORT3", "ES1_CAM3") + +# ADEuresysConfig2(portName, interfaceIndex, deviceIndex, numBuffers, maxMemory, priority, stackSize) +ADEuresysConfig2("$(PORT3)", $(INTERFACE_ID), 2, 100, 0, 0, 0) +asynSetTraceIOMask($(PORT3), 0, ESCAPE) + +# Load database records for Camera 3 +dbLoadRecords("$(ADEURESYS)/db/Euresys.template", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") +dbLoadRecords("$(GENICAM_DB_FILE)", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDevice.template", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLDataStream.template", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLInterface.template", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") +dbLoadRecords("$(ADGENICAM)/db/Euresys_Coaxlink_TLSystem.template", "P=$(PREFIX),R=cam3:,PORT=$(PORT3)") + +# Camera 3 plugins +NDStdArraysConfigure("Image3", 5, 0, "$(PORT3)", 0, 0) +dbLoadRecords("$(ADCORE)/db/NDStdArrays.template", "P=$(PREFIX),R=image3:,PORT=Image3,ADDR=0,TIMEOUT=1,NDARRAY_PORT=$(PORT3),TYPE=Int8,FTVL=CHAR,NELEMENTS=$(NELEMENTS)") + +#============================================================================== +# Common plugins (can be loaded once for all cameras or per camera as needed) +#============================================================================== +# Uncomment to load common plugins for the first camera +# set_requestfile_path("$(ADGENICAM)/db") +# set_requestfile_path("$(ADEURESYS)/db") +# < $(ADCORE)/iocBoot/commonPlugins.cmd + +iocInit() + +# Save settings every thirty seconds +create_monitor_set("auto_settings.req", 30,"P=$(PREFIX)") From 3858587369f5e94a863e4c5d43fdc553466fa8ef Mon Sep 17 00:00:00 2001 From: Jan Jug Date: Fri, 13 Feb 2026 11:48:52 +0100 Subject: [PATCH 2/2] Expand the documetation for multi-camera support An explanation of the added IOC configuration command has been added to the documentation. --- docs/ADEuresys/ADEuresys.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/ADEuresys/ADEuresys.rst b/docs/ADEuresys/ADEuresys.rst index 2ac94c7..09c9271 100644 --- a/docs/ADEuresys/ADEuresys.rst +++ b/docs/ADEuresys/ADEuresys.rst @@ -188,6 +188,29 @@ multiple cameras from one computer. ``stackSize`` is the stack size. 0 means medium size. +A secondary command is available to allow for the selection of both the interface (card) and device (camera):: + + ADEuresysConfig2(const char *portName, int interfaceIndex, int deviceIndex, + int numEGBuffers, size_t maxMemory, int priority, int stackSize) + +``portName`` is the name for the ADEuresys port driver + +``interfaceIndex`` is used to select the framegrabber card. +If there is only one framegrabber card, use 0. If there are multiple, you can use the EGrabber_ utilities (such as +eGrabber Studio) to figure out the topology of the system. + +``deviceIndex`` is used to select the camera on the chosen framegrabber. + +``numEGBuffers`` is the number of buffers to allocate in EGrabber. If set to 0 or omitted the default of 100 will be used. + +``maxMemory`` is the maximum amount of memory the NDArrayPool is allowed to allocate. 0 means unlimited. + +``priority`` is the priority of the port thread. 0 means medium priority. + +``stackSize`` is the stack size. 0 means medium size. + +An example for setting up three cameras on one framegrabber can be found in the Adimec example IOC directory. + Cameras Tested -------------- ADEuresys_ has been tested with 3 very different cameras, shown in the following table.