Jump to content

labview queue from native windows dll


Recommended Posts

I am developing a LabView interface to a legacy device driver which is in the form of a native Windows DLL.

The heart of the device driver is a callback function that periodically delivers data (video frames) from the device (a camera). I have a written a wrapper DLL which implements the callback function, puts the frames on a queue, and exports a function that can be called from LV to pop a frame from the queue. On the LV side I have a thread that polls for frames by calling the "pop frame" function in the wrapper DLL and then puts the frames in a LabView Queue, which is the interface to the rest of the LabView application.

My question is: what event-driven alternatives to my polling solution are available?

I've searched through various fora and seen many discussions on this subject, but the answers haven't been very coherent (at least to me), and many of the discussions are quite old.

Ideally I'd like to be able to push frames onto a LabView Queue from the wrapper DLL, thus entirely eliminating the thread I have running on the LV side. Are LabView Queues exposed to a Windows DLL in any way?

Failing that, what techniques are available for an event-driven .. as opposed to polled .. solution? Notifiers?

Thanks,

Mark Z.

Link to comment

Ideally I'd like to be able to push frames onto a LabView Queue from the wrapper DLL, thus entirely eliminating the thread I have running on the LV side. Are LabView Queues exposed to a Windows DLL in any way?

Failing that, what techniques are available for an event-driven .. as opposed to polled .. solution? Notifiers?

To my knowledge there is no documented C API to the LabVIEW queues. However there is this function PostLVUserEvent() that basically sends back an user event to LabVIEW. This can then be catched with an event structure.

Try searching for PostLVUserEvent either here and on the NI LabVIEW forum and you should get a whole bunch of discussions and even an example or two from where you can pick up.

Link to comment

I am developing a LabView interface to a legacy device driver which is in the form of a native Windows DLL.

The heart of the device driver is a callback function that periodically delivers data (video frames) from the device (a camera). ....

My question is: what event-driven alternatives to my polling solution are available?

This is very common issue for writing camera drivere. Here is my solution (for iCube camera):

Callback function:

DLL void iCubeCallback (char *pBuffer, int IBufferSize, LVUserEventRef *eventRef){	struct tBuff {		char *pBuff;		int size;	}  buff;	buff.pBuff=pBuffer;	buff.size=IBufferSize;	PostLVUserEvent(*eventRef, (void *)&buff);    	}

Initialization:

post-7450-0-78593800-1292235484_thumb.pn

And Get Frame.vi:

post-7450-0-71046200-1292235599_thumb.pn

To my knowledge there is no documented C API to the LabVIEW queues.

Do you know any undocumented functions for that?

  • Like 1
Link to comment

Could you also show us the code for the ICubeSDK_SetCallback function?

I am excited about using this same idea to create a LV user event from a NSV value change callback registered within a CVI dll

I don't have it, as it is a part of camera SDK provided by manufacturer. I only call the DLL. It takes a pointer to a callback function which is (internally by SDK) called each time new frame arrives. I made separate dll which contains only the callback function I listed before.

Link to comment

DLL void iCubeCallback (char *pBuffer, int IBufferSize, LVUserEventRef *eventRef){	struct tBuff {		char *pBuff;		int size;	}  buff;	buff.pBuff=pBuffer;	buff.size=IBufferSize;	PostLVUserEvent(*eventRef, (void *)&buff);    	}

If you changed the user Event to be of a string instead of the cluster and changed the code in the Callback like this:

DLL void iCubeCallback (char *pBuffer, int IBufferSize, LVUserEventRef *eventRef){    LStrHandle *pBuf = NULL;	MgErr err = NumericArrayResize(uB, 1, (UHandle*)pBuf, IBufferSize);	if (noErr == err)	{    	MoveBlock(pBuffer, LStrBuf(**pBuf), IBufferSize);    	LStrLen(**pBuf) = IBufferSize;    	PostLVUserEvent(*eventRef, (void *)pBuf); 	}  	}

This way you receive the data string directly in the event structure and don't need to invoke buffer copy functions there.

Do you know any undocumented functions for that?

No not really. Supposedly there would be probably some functions in the latest LabVIEW versions, but without at least some form of header file available it's simply to much of trial and crash :ph34r:.

Link to comment

If you changed the user Event to be of a string instead of the cluster and changed the code in the Callback like this:

DLL void iCubeCallback (char *pBuffer, int IBufferSize, LVUserEventRef *eventRef){    LStrHandle *pBuf = NULL;	MgErr err = NumericArrayResize(uB, 1, (UHandle*)pBuf, IBufferSize);	if (noErr == err)	{    	MoveBlock(pBuffer, LStrBuf(**pBuf), IBufferSize);    	LStrLen(**pBuf) = IBufferSize;    	PostLVUserEvent(*eventRef, (void *)pBuf); 	}  	}

This way you receive the data string directly in the event structure and don't need to invoke buffer copy functions there.

In fact, it is more convenient. But (just to make things clear) - the buffer is still copied here, so (taking into account typical sizes of frames) there should be no performance differences between both versions.

Link to comment

In fact, it is more convenient. But (just to make things clear) - the buffer is still copied here, so (taking into account typical sizes of frames) there should be no performance differences between both versions.

Yes you can't avoid a buffer copy but I wasn't sure if your buffer copy VIs did just a single buffer copy or if your first VI does some copying too. But the main reason to do it in this way is simply to keep as much of the low level stuff in the DLL, especially since you have that DLL already anyhow.

And to be honest, I believe your original solution could have a bug. You pass in a structure that is on the stack. The user event however is processed asynchronous so at the time the event is read the stack location can have long been invalidated and possibly overwritten with other stuff. Bad, Bad! especially since both information relate to a memory buffer.

And declaring the variable static is also not a solution since a new event could be generated before the previous is processed.

Note: It may work if PostLVUserEvent makes a copy of the data, leaving the ownership of the data object to its caller. It could do that since the event refnum stores internally the typecode of the data object associated with it, but I'm not 100% sure it does so.

Link to comment

Yes you can't avoid a buffer copy but I wasn't sure if your buffer copy VIs did just a single buffer copy or if your first VI does some copying too.

The purple VIs in Get Frame.vi are part of MemBlock library and they simply call MoveBlock function.

But the main reason to do it in this way is simply to keep as much of the low level stuff in the DLL, especially since you have that DLL already anyhow.

I fully agree

Link to comment
  • 1 month later...

Hi,

Firstly a couple of apologies,

1) Sorry for hijacking the thread, vugie you have done what I'm trying to do.

2) My C++ know-how isn't great, which is why I'm stumped.

So, on to the problem.

I'm trying to grab frames from an ICube camera using the callback function and LabVIEW events.

I've tried using the string version of the Callback function as suggested by Rolf, but have had trouble with the code you posted. My version is below. I haven't actually tried passing the string data to the LabVIEW User event becasue the Callback function fails before it gets to point where it is ready to post the data. Instead I've set up the user event to accept an I32 and I post a series of dummy events to see how far through the callback function I get. Here's my code, adapted from Rolf's code (note I use lBufferSize, not IBufferSize, took me a while to notice that difference).

long CALLBACK MyCallbackFunc(char * pBuffer, long lBufferSize, LVUserEventRef *eventRef ){	//send dummy event	int param = 1;	PostLVUserEvent(*eventRef, (void *)&param);     		LStrHandle *pBuf = NULL;		//send dummy event	param = 2;	PostLVUserEvent(*eventRef, (void *)&param);			MgErr err = NumericArrayResize(uB, 1, (UHandle*)&pBuf, lBufferSize);		//send dummy event	param = 3;	PostLVUserEvent(*eventRef, (void *)&param);	if (noErr == err)    {	//send dummy event	param = 4;	PostLVUserEvent(*eventRef, (void *)&param);    	//MoveBlock(pBuffer, LStrBuf(**pBuf), lBufferSize);	MoveBlock((void *)pBuffer, (void *)*pBuf, lBufferSize);		//send dummy event	param = 5;	PostLVUserEvent(*eventRef, (void *)&param);	LStrLen(**pBuf) = lBufferSize;	//PostLVUserEvent(*eventRef, (void *)pBuf);		//send dummy event	param = 6;	PostLVUserEvent(*eventRef, (void *)&param); 	}	return 42;}

Initially Rolf's code failed at NumericArrayResize(). I got it working by changing the second parameter to (UHandle*)&pBuf. Is this the right thing to do?

Now it fails at LStrLen(**pBuf) = lBufferSize;

I've tried changing various things (more *, less *, adding (void *) etc but no luck, and to be honest even if it did work I wouldn't know if it was doing the right thing anyway. So any help would be appreciated.

I'm using LabVIEW 8.6 and Visual Studio 2010

Thanks

David

Link to comment

...

Initially Rolf's code failed at NumericArrayResize(). I got it working by changing the second parameter to (UHandle*)&pBuf. Is this the right thing to do?

Now it fails at LStrLen(**pBuf) = lBufferSize;

It should be like Rolf wrote. pBuf is already defined as pointer to handle, so no need to use & here. What error do you get?

I don't understand LStrLen(**pBuf) = lBufferSize;

LStrLen returns the lenght of a string as a number. = makes no sense in this context. It souldn't even compile in my opinion.

What would make sense is **pBuf.cnt = lBufferSize; which writes length of the string at its begining, but I guess that NumericArrayResize() should do it.

BTW the frame data is in BGR order (API documentation "suggests" that it is RGB)

Link to comment

It should be like Rolf wrote. pBuf is already defined as pointer to handle, so no need to use & here. What error do you get?

Right!

I don't understand LStrLen(**pBuf) = lBufferSize;

LStrLen returns the lenght of a string as a number. = makes no sense in this context. It souldn't even compile in my opinion.

What would make sense is **pBuf.cnt = lBufferSize; which writes length of the string at its begining, but I guess that NumericArrayResize() should do it.

No! NumericArrayResize() does not update the length value in an array. Read the documentation about that function which states so explicitedly. One of the likely reasons for this is that you want to update the length usually after you have added the elements to the array and not before. Otherwise leaving the routine early because of an error or exception will leave uninitialized array elements in the buffer that according to the length parameter should be there.

And LStrLen(ptr) is simply a macro and not a function. It translates to

#define LStrLen(ptr) ((LStrPtr)(ptr))->len;

so it can be used both as lvalue and rvalue in a statement.

Link to comment

Hi,

  	//MoveBlock(pBuffer, LStrBuf(**pBuf), lBufferSize);	MoveBlock((void *)pBuffer, (void *)*pBuf, lBufferSize);

If you thinker with the code during debugging you should make sure to revert such changes before claiming it doesn't work.

The commented out call to MoveBlock() is clearly the one you want to have here once you have corrected the call to NumericArrayResize() to take the pBuf argument without pointer reference as it should.

Then LStrLen() later on will be able to reference the length parameter of the buffer and not some random value in memory

Yes pointer handling is not easy and advanced pointer handling as used by LabVIEW internally to allow decent performance even less. That is why we all program in LabVIEW after all, isn't it? :D

Link to comment

No! NumericArrayResize() does not update the length value in an array. LStrLen(ptr) is simply a macro and not a function. It translates to

#define LStrLen(ptr) ((LStrPtr)(ptr))->len;

so it can be used both as lvalue and rvalue in a statement.

Ok, I got it. However in my extcode.h it is somewhat different:

#define LStrLen(sp) (((sp))->cnt) - no casting

Naming convention is not too good in extcode, BTW

Link to comment

Firstly, thanks for your comments and I appreciate your help.

It should be like Rolf wrote. pBuf is already defined as pointer to handle, so no need to use & here. What error do you get?

I'm sure it should and I suspect it is something that I'm doing wrong. I started from the beginning again with rolfs code and it fails at every line apart from the first. By making the changes I mentioned previously I can at least get each line to pass, whether they do something sensible is another matter.

I have a test VI where I set up the camera and can then start and trigger the image stream.

I display the image in the default preview window.

I have the trigger set to software trigger.

Without the callback set the image updates every time I generate a software trigger

With the callback set the image does not update and after the first software trigger I get a popup box with the title NET_USB_CMO.dll and the message "Error: Closing Thread". That dll appears to be part of the camera driver software.

Subsequent software triggers don't appear to do anything, but the call to ICubeSDK_SetTrigger returns 0, (no error).

I get the same failure regardless of which part of the callback code the program gets to.

Is there something I should be setting in my callback dll project properties or some other setting that is needed?.

BTW the frame data is in BGR order (API documentation "suggests" that it is RGB)

I have a 5MP monochrome camera, I've been using CALLBACK_RAW (=0) as the grabmode. I've also tried various image modes (640x480 etc), all give the same results.

The commented out call to MoveBlock() is clearly the one you want to have here once you have corrected the call to NumericArrayResize() to take the pBuf argument without pointer reference as it should.

Then LStrLen() later on will be able to reference the length parameter of the buffer and not some random value in memory

Again, I'm sure it is. I forgot to mention that change in my original post. I made it in order to get the line to pass, but I guessed it was probably doing something random.

I've also tried to write a console application version of the code but I get the Fatal Error that extcode.dll was called from outside LabVIEW, which is understandable, but is there a way round that?

My dll dynamically links to ICubeSDK.dll. Could this be an issue? I'm trying a static linking version but am having differnt compile issues.

David

Link to comment

Ok, I got it. However in my extcode.h it is somewhat different:

#define LStrLen(sp) (((sp))->cnt) - no casting

Naming convention is not too good in extcode, BTW

Naming conventions on a multiplatform project are really difficult to do. Windows C programmers use completely different naming convention than Macintosh C programmers, and they are again very different from Unix C programmers. So which one is the right for LabVIEW?

LabVIEW traditionally used the old Macintosh Classic naming conventions for most things in its underlying C code interface, which does not use the awful Hungarian notation of Windows and also not the everything_except_CONSTANT_NAMES is lowercase mode of Unix.

Which one is better? Well I don't like both Hungarian notation as well as unix_all_is_lowercase().

Link to comment

Naming conventions on a multiplatform project are really difficult to do. Windows C programmers use completely different naming convention than Macintosh C programmers, and they are again very different from Unix C programmers. So which one is the right for LabVIEW?

LabVIEW traditionally used the old Macintosh Classic naming conventions for most things in its underlying C code interface, which does not use the awful Hungarian notation of Windows and also not the everything_except_CONSTANT_NAMES is lowercase mode of Unix.

Which one is better? Well I don't like both Hungarian notation as well as unix_all_is_lowercase().

I meant rather that macro is undifferentiable to functions, there is no general prefix to protect against naming conflicts, and there are no prefixes to group related functions (i.e. memory manager)

Link to comment

Again, I'm sure it is. I forgot to mention that change in my original post. I made it in order to get the line to pass, but I guessed it was probably doing something random.

Well I can't tell you what is wrong except that what I have initially provided to you is for 99% certain more correct than what you have shown in the last sample code.

I've also tried to write a console application version of the code but I get the Fatal Error that extcode.dll was called from outside LabVIEW, which is understandable, but is there a way round that?

No, there is no way to use LabVIEW manager calls in a non-LabVIEW process. How should that work? Someone has to provide the implementation of those functions and that is either LabVIEW.exe or lvrt.dll. However lvrt.dll is not a standalone executable but just the runtime engine for LabVIEW and needs a very specific way to be initialized in order to work properly. The only one who knows how this initialization has to be done are the programmers at NI, and it changes for sure with almost every version of LabVIEW. The knowledge about this is all stuffed into the little startup stub that the LabVIEW application builder will tag to your executable. This startup stub is specifically meant to initialize the process as GUI process so linking it to your console program is also not a real option.

However your code seems to indicate that you have never ventured into source code level debugging. But that is certainly a much better way to debug external code than trying to send some debug print messages to the LabVIEW task and hoping to decipher from that what might be go wrong in the external code.

Link to comment

Well I can't tell you what is wrong except that what I have initially provided to you is for 99% certain more correct than what you have shown in the last sample code.

Fair enough. I have created a test dll with just the NumericArrayResize function part of the code, below. This also fails. Could it be a corrupt labview.lib, extcode.h file? Is it worth reinstalling LabVIEW (I'd rather avoid this if possible)

// NumArrResizeTrial.cpp #include "stdafx.h"#include "extcode.h"extern "C" _declspec (dllexport) int NumArrResizeTest(int IBufferSize);BOOL APIENTRY DllMain( HMODULE hModule,           			DWORD  ul_reason_for_call,           			LPVOID lpReserved			         ){	return TRUE;}_declspec (dllexport) int NumArrResizeTest(int IBufferSize){	int ReturnValue = 0;	LStrHandle *pBuf = NULL;		MgErr err = NumericArrayResize(uB, 1, (UHandle*)pBuf, IBufferSize);		if (err == noErr)	{		ReturnValue = 1;	}	return ReturnValue;}

However your code seems to indicate that you have never ventured into source code level debugging. But that is certainly a much better way to debug external code than trying to send some debug print messages to the LabVIEW task and hoping to decipher from that what might be go wrong in the external code.

You're right, C++ source code level debugging is new to me. I'm working on it but not there yet.

dave

Link to comment

OK, I have taken a different approach, using what was described in this post.

When I use moveblock to copy the data from pBuffer to pixelData have I referenced pixelData correctly?. The code runs and returns an array of the correct size filled with sensible values so I'm happy with that. I'm concerend I'm screwing around with the memory allocation.

All comments appreciated

dave

...// 1D U8 arraytypedef struct {	int32		length;      	// Length of array	uInt8		pixelData[1];				// Array} arrU81D, *arrU81DP, **arrU81DH;// Cluster with a scalar and handle to a 1D U8 arraytypedef struct {	int32		numPixels;				// number of pixels	arrU81DH	pixelDataArrHdl;		// Pixel Data Array} clustB, *clustBP,**clustBH;......//-----------------------------------------------------// Callback Function//-----------------------------------------------------//Sends the Pixel Data to a LV Event.long CALLBACK MyCallbackFunc(char * pBuffer, long lBufferSize, LVUserEventRef *eventRef ){	clustBP eventClusterP;    	// Allocate cluster pointer    	eventClusterP=(clustBP)DSNewPtr(sizeof(int32)+sizeof(arrU81DH));   		    	// Allocate pixel data array handle    	eventClusterP->pixelDataArrHdl=(arrU81DH)DSNewHandle(sizeof(int32)+lBufferSize*sizeof(uInt8));       	// Set PixelDataArray length field    	(*(eventClusterP->pixelDataArrHdl))->length = lBufferSize;	//copy pBuffer to pixelDataArray    	MoveBlock(pBuffer, (*(eventClusterP->pixelDataArrHdl))->pixelData , lBufferSize);   //<--- Is this correct, i.e. safe			// Set dummy value       	eventClusterP->numPixels = lBufferSize;    	// Send cluster via event    	PostLVUserEvent(*eventRef,(void *)eventClusterP);    	// Clear array handle and cluster pointer    	DSDisposeHandle(eventClusterP->pixelDataArrHdl);    	DSDisposePtr(eventClusterP);	return 1;}

Link to comment

Fair enough. I have created a test dll with just the NumericArrayResize function part of the code, below. This also fails. Could it be a corrupt labview.lib, extcode.h file? Is it worth reinstalling LabVIEW (I'd rather avoid this if possible)

What does fail and how? What do you specify as size? Instead of translating the return value of NumericArrayResize() into just a boolean you could much better return the error code directly. That might give you a bit more feedback to the cause of a possible error. And of course I hope you are not executing this test many times, because you leak the handle every time :-)

OK, I have taken a different approach, using what was described in this post.

When I use moveblock to copy the data from pBuffer to pixelData have I referenced pixelData correctly?. The code runs and returns an array of the correct size filled with sensible values so I'm happy with that. I'm concerend I'm screwing around with the memory allocation.

All comments appreciated

dave

...// 1D U8 arraytypedef struct {	int32		length;      	// Length of array	uInt8		pixelData[1];				// Array} arrU81D, *arrU81DP, **arrU81DH;// Cluster with a scalar and handle to a 1D U8 arraytypedef struct {	int32		numPixels;				// number of pixels	arrU81DH	pixelDataArrHdl;		// Pixel Data Array} clustB, *clustBP,**clustBH;......//-----------------------------------------------------// Callback Function//-----------------------------------------------------//Sends the Pixel Data to a LV Event.long CALLBACK MyCallbackFunc(char * pBuffer, long lBufferSize, LVUserEventRef *eventRef ){	clustBP eventClusterP;    	// Allocate cluster pointer    	eventClusterP=(clustBP)DSNewPtr(sizeof(int32)+sizeof(arrU81DH));   		    	// Allocate pixel data array handle    	eventClusterP->pixelDataArrHdl=(arrU81DH)DSNewHandle(sizeof(int32)+lBufferSize*sizeof(uInt8));       	// Set PixelDataArray length field    	(*(eventClusterP->pixelDataArrHdl))->length = lBufferSize;	//copy pBuffer to pixelDataArray    	MoveBlock(pBuffer, (*(eventClusterP->pixelDataArrHdl))->pixelData , lBufferSize);   //<--- Is this correct, i.e. safe			// Set dummy value       	eventClusterP->numPixels = lBufferSize;    	// Send cluster via event    	PostLVUserEvent(*eventRef,(void *)eventClusterP);    	// Clear array handle and cluster pointer    	DSDisposeHandle(eventClusterP->pixelDataArrHdl);    	DSDisposePtr(eventClusterP);	return 1;}

This looks ok to me.

Some minor remarks. The dummy numPixels would not be necessary since it contains the same value as the handle size itself. Instead you could define the event to be simply a byte array and just pass the handle to PostLVUserEvent().

I make it a habit to change the handle size after I have filled in the data into the handle. That may seem just cosmetics and as long as you have arrays of skalar values that is indeed so, but if you talk about arrays of strings or other handles then there is a subtle difference if your loop aborts prematurely because of an error. If you have set the size before it may indicate a larger array than the loop effectively filled in and that could cause LabVIEW to try to access those not yet initialized values, causing crashes or other nasty things.

Link to comment

What does fail and how? What do you specify as size? Instead of translating the return value of NumericArrayResize() into just a boolean you could much better return the error code directly. That might give you a bit more feedback to the cause of a possible error. And of course I hope you are not executing this test many times, because you leak the handle every time :-)

The VI, which is just a call library function node with all inputs and outputs wired, executes but the error cluster shows code 1097 which is:

An exception occurred within the external code called by a Call Library Function Node. The exception may have corrupted the LabVIEW memory. Save any work to a new location and restart LabVIEW.

I pass a U32 into the CLFN for IBufferSize. It doesn't matter what value I pass in, the error always occurs.

I changed the code to returrn err, this is always 0.

To dispose of the pointer: DSDisposeHandle(pBuf); or DSDisposeHandle(*pBuf);?

This looks ok to me.

Some minor remarks. The dummy numPixels would not be necessary since it contains the same value as the handle size itself. Instead you could define the event to be simply a byte array and just pass the handle to PostLVUserEvent().

Yes, I realsied numPixels is redundant. I've got a version of the code with just the array but here I've left it in more as an exercise and to allow for the fact I may want to pass out other more useful data in the future.

Link to comment

The VI, which is just a call library function node with all inputs and outputs wired, executes but the error cluster shows code 1097 which is:

An exception occurred within the external code called by a Call Library Function Node. The exception may have corrupted the LabVIEW memory. Save any work to a new location and restart LabVIEW.

I pass a U32 into the CLFN for IBufferSize. It doesn't matter what value I pass in, the error always occurs.

I changed the code to returrn err, this is always 0.

Well this may be redundantet but have you checked the calling convention? MSVC by default does compile to use cdecl convention, but can be configured to use another default calling configuration for a project. Since your function does not specify any calling convention it will be compiled into the default calling convention and the error you see often gets caused by wrong calling convention configuration. Basically it messes up your call stack and that is quite fatal eventhough LabVIEW tries to protect you from this with an exception handler.

To dispose of the pointer: DSDisposeHandle(pBuf); or DSDisposeHandle(*pBuf);?

DSDisposeHandle() takes the handle directly so assuming your earlier code it should be DSDisposeHandle(*pBuf); since pBuf is a pointer to a handle. But you should of course only call DSDisposeHandle() when NumericArrayResize() was successful or otherwise you get another nasty error.

It's simply the standard trouble about C programming and what I usually end up doing is a more or less deeply intended code where the according cleanup operations are done one level deeper than where the resource was created. Some people prefer to have some sort of goto fail; whenever an error occurred, but coming from a strict Pascal schooling I still find gotos one of the biggest evils in a programming language. :rolleyes: (quickly followed by application global variables)

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.