// Copyright (C) 2015 Marek Kowalski (M.Kowalski@ire.pw.edu.pl), Jacek Naruniec (J.Naruniec@ire.pw.edu.pl) // License: MIT Software License See LICENSE.txt for the full license. // If you use this software in your research, then please use the following citation: // Kowalski, M.; Naruniec, J.; Daniluk, M.: "LiveScan3D: A Fast and Inexpensive 3D Data // Acquisition System for Multiple Kinect v2 Sensors". in 3D Vision (3DV), 2015 International Conference on, Lyon, France, 2015 // @INPROCEEDINGS{Kowalski15, // author={Kowalski, M. and Naruniec, J. and Daniluk, M.}, // booktitle={3D Vision (3DV), 2015 International Conference on}, // title={LiveScan3D: A Fast and Inexpensive 3D Data Acquisition System for Multiple Kinect v2 Sensors}, // year={2015}, // } #include "stdafx.h" #include "resource.h" #include "LiveScanClient.h" #include "filter.h" #include #include #include std::mutex m_mSocketThreadMutex; int APIENTRY wWinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd ) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); LiveScanClient application; application.Run(hInstance, nShowCmd); } LiveScanClient::LiveScanClient() : m_hWnd(NULL), m_nLastCounter(0), m_nFramesSinceUpdate(0), m_fFreq(0), m_nNextStatusTime(0LL), m_pD2DFactory(NULL), m_pDrawColor(NULL), m_pDepthRGBX(NULL), m_pCameraSpaceCoordinates(NULL), m_pColorCoordinatesOfDepth(NULL), m_pDepthCoordinatesOfColor(NULL), m_bCalibrate(false), m_bFilter(false), m_bStreamOnlyBodies(false), m_bCaptureFrame(false), m_bConnected(false), m_bConfirmCaptured(false), m_bConfirmCalibrated(false), m_bShowDepth(false), m_bSocketThread(true), m_pClientSocket(NULL), m_nFilterNeighbors(10), m_fFilterThreshold(0.01f) { pCapture = new KinectCapture(); LARGE_INTEGER qpf = {0}; if (QueryPerformanceFrequency(&qpf)) { m_fFreq = double(qpf.QuadPart); } m_vBounds.push_back(-0.5); m_vBounds.push_back(-0.5); m_vBounds.push_back(-0.5); m_vBounds.push_back(0.5); m_vBounds.push_back(0.5); m_vBounds.push_back(0.5); } LiveScanClient::~LiveScanClient() { // clean up Direct2D renderer if (m_pDrawColor) { delete m_pDrawColor; m_pDrawColor = NULL; } if (pCapture) { delete pCapture; pCapture = NULL; } if (m_pDepthRGBX) { delete[] m_pDepthRGBX; m_pDepthRGBX = NULL; } if (m_pCameraSpaceCoordinates) { delete[] m_pCameraSpaceCoordinates; m_pCameraSpaceCoordinates = NULL; } if (m_pColorCoordinatesOfDepth) { delete[] m_pColorCoordinatesOfDepth; m_pColorCoordinatesOfDepth = NULL; } if (m_pDepthCoordinatesOfColor) { delete[] m_pDepthCoordinatesOfColor; m_pDepthCoordinatesOfColor = NULL; } if (m_pClientSocket) { delete m_pClientSocket; m_pClientSocket = NULL; } // clean up Direct2D SafeRelease(m_pD2DFactory); } int LiveScanClient::Run(HINSTANCE hInstance, int nCmdShow) { MSG msg = {0}; WNDCLASS wc; // Dialog custom window class ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW | CS_VREDRAW; wc.cbWndExtra = DLGWINDOWEXTRA; wc.hCursor = LoadCursorW(NULL, IDC_ARROW); wc.hIcon = LoadIconW(hInstance, MAKEINTRESOURCE(IDI_APP)); wc.lpfnWndProc = DefDlgProcW; wc.lpszClassName = L"LiveScanClientAppDlgWndClass"; if (!RegisterClassW(&wc)) { return 0; } // Create main application window HWND hWndApp = CreateDialogParamW( NULL, MAKEINTRESOURCE(IDD_APP), NULL, (DLGPROC)LiveScanClient::MessageRouter, reinterpret_cast(this)); // Show window ShowWindow(hWndApp, nCmdShow); std::thread t1(&LiveScanClient::SocketThreadFunction, this); // Main message loop while (WM_QUIT != msg.message) { //HandleSocket(); UpdateFrame(); while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { // If a dialog message will be taken care of by the dialog proc if (hWndApp && IsDialogMessageW(hWndApp, &msg)) { continue; } TranslateMessage(&msg); DispatchMessageW(&msg); } } m_bSocketThread = false; t1.join(); return static_cast(msg.wParam); } void LiveScanClient::UpdateFrame() { if (!pCapture->bInitialized) { return; } bool bNewFrameAcquired = pCapture->AcquireFrame(); if (!bNewFrameAcquired) return; pCapture->MapDepthFrameToCameraSpace(m_pCameraSpaceCoordinates); pCapture->MapDepthFrameToColorSpace(m_pColorCoordinatesOfDepth); { std::lock_guard lock(m_mSocketThreadMutex); StoreFrame(m_pCameraSpaceCoordinates, m_pColorCoordinatesOfDepth, pCapture->pColorRGBX, pCapture->vBodies, pCapture->pBodyIndex); if (m_bCaptureFrame) { m_vGatheredVertices.push_back(m_vLastFrameVertices); m_vGatheredRGBPoints.push_back(m_vLastFrameRGB); m_bConfirmCaptured = true; m_bCaptureFrame = false; } } if (m_bCalibrate) { std::lock_guard lock(m_mSocketThreadMutex); Point3f *pCameraCoordinates = new Point3f[pCapture->nColorFrameWidth * pCapture->nColorFrameHeight]; pCapture->MapColorFrameToCameraSpace(pCameraCoordinates); bool res = calibration.Calibrate(pCapture->pColorRGBX, pCameraCoordinates, pCapture->nColorFrameWidth, pCapture->nColorFrameHeight); delete[] pCameraCoordinates; if (res) { m_bConfirmCalibrated = true; m_bCalibrate = false; } } if (!m_bShowDepth) ProcessColor(pCapture->pColorRGBX, pCapture->nColorFrameWidth, pCapture->nColorFrameHeight); else ProcessDepth(pCapture->pDepth, pCapture->nDepthFrameWidth, pCapture->nDepthFrameHeight); ShowFPS(); } LRESULT CALLBACK LiveScanClient::MessageRouter(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LiveScanClient* pThis = NULL; if (WM_INITDIALOG == uMsg) { pThis = reinterpret_cast(lParam); SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(pThis)); } else { pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA)); } if (pThis) { return pThis->DlgProc(hWnd, uMsg, wParam, lParam); } return 0; } LRESULT CALLBACK LiveScanClient::DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(wParam); UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: { // Bind application window handle m_hWnd = hWnd; // Init Direct2D D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); // Get and initialize the default Kinect sensor bool res = pCapture->Initialize(); if (res) { m_pDepthRGBX = new RGB[pCapture->nColorFrameWidth * pCapture->nColorFrameHeight]; m_pCameraSpaceCoordinates = new Point3f[pCapture->nDepthFrameWidth * pCapture->nDepthFrameHeight]; m_pColorCoordinatesOfDepth = new Point2f[pCapture->nDepthFrameWidth * pCapture->nDepthFrameHeight]; m_pDepthCoordinatesOfColor = new Point2f[pCapture->nColorFrameWidth * pCapture->nColorFrameHeight]; } else { SetStatusMessage(L"Capture device failed to initialize!", 10000, true); } // Create and initialize a new Direct2D image renderer (take a look at ImageRenderer.h) // We'll use this to draw the data we receive from the Kinect to the screen HRESULT hr; m_pDrawColor = new ImageRenderer(); hr = m_pDrawColor->Initialize(GetDlgItem(m_hWnd, IDC_VIDEOVIEW), m_pD2DFactory, pCapture->nColorFrameWidth, pCapture->nColorFrameHeight, pCapture->nColorFrameWidth * sizeof(RGB)); if (FAILED(hr)) { SetStatusMessage(L"Failed to initialize the Direct2D draw device.", 10000, true); } ReadIPFromFile(); } break; // If the titlebar X is clicked, destroy app case WM_CLOSE: WriteIPToFile(); DestroyWindow(hWnd); break; case WM_DESTROY: // Quit the main message pump PostQuitMessage(0); break; // Handle button press case WM_COMMAND: if (IDC_BUTTON_CONNECT == LOWORD(wParam) && BN_CLICKED == HIWORD(wParam)) { std::lock_guard lock(m_mSocketThreadMutex); if (m_bConnected) { delete m_pClientSocket; m_pClientSocket = NULL; m_bConnected = false; SetDlgItemTextA(m_hWnd, IDC_BUTTON_CONNECT, "Connect"); } else { m_vGatheredVertices.clear(); m_vGatheredRGBPoints.clear(); try { char address[20]; GetDlgItemTextA(m_hWnd, IDC_IP, address, 20); m_pClientSocket = new SocketClient(address, 48001); m_bConnected = true; SetDlgItemTextA(m_hWnd, IDC_BUTTON_CONNECT, "Disconnect"); //Clear the status bar so that the "Failed to connect..." disappears. SetStatusMessage(L"", 1, true); } catch (...) { SetStatusMessage(L"Failed to connect. Did you start the server?", 10000, true); } } } if (IDC_BUTTON_SWITCH == LOWORD(wParam) && BN_CLICKED == HIWORD(wParam)) { m_bShowDepth = !m_bShowDepth; if (m_bShowDepth) { SetDlgItemTextA(m_hWnd, IDC_BUTTON_SWITCH, "Show color"); } else { SetDlgItemTextA(m_hWnd, IDC_BUTTON_SWITCH, "Show depth"); } } break; } return FALSE; } void LiveScanClient::ProcessDepth(const UINT16* pBuffer, int nWidth, int nHeight) { // Make sure we've received valid data if (m_pDepthRGBX && m_pDepthCoordinatesOfColor && pBuffer && (nWidth == pCapture->nDepthFrameWidth) && (nHeight == pCapture->nDepthFrameHeight)) { // end pixel is start + width*height - 1 const UINT16* pBufferEnd = pBuffer + (nWidth * nHeight); pCapture->MapColorFrameToDepthSpace(m_pDepthCoordinatesOfColor); for (int i = 0; i < pCapture->nColorFrameWidth * pCapture->nColorFrameHeight; i++) { Point2f depthPoint = m_pDepthCoordinatesOfColor[i]; BYTE intensity = 0; if (depthPoint.X >= 0 && depthPoint.Y >= 0) { int depthIdx = (int)(depthPoint.X + depthPoint.Y * pCapture->nDepthFrameWidth); USHORT depth = pBuffer[depthIdx]; intensity = static_cast(depth % 256); } m_pDepthRGBX[i].rgbRed = intensity; m_pDepthRGBX[i].rgbGreen = intensity; m_pDepthRGBX[i].rgbBlue = intensity; } // Draw the data with Direct2D m_pDrawColor->Draw(reinterpret_cast(m_pDepthRGBX), pCapture->nColorFrameWidth * pCapture->nColorFrameHeight * sizeof(RGB), pCapture->vBodies); } } void LiveScanClient::ProcessColor(RGB* pBuffer, int nWidth, int nHeight) { // Make sure we've received valid data if (pBuffer && (nWidth == pCapture->nColorFrameWidth) && (nHeight == pCapture->nColorFrameHeight)) { // Draw the data with Direct2D m_pDrawColor->Draw(reinterpret_cast(pBuffer), pCapture->nColorFrameWidth * pCapture->nColorFrameHeight * sizeof(RGB), pCapture->vBodies); } } bool LiveScanClient::SetStatusMessage(_In_z_ WCHAR* szMessage, DWORD nShowTimeMsec, bool bForce) { INT64 now = GetTickCount64(); if (m_hWnd && (bForce || (m_nNextStatusTime <= now))) { SetDlgItemText(m_hWnd, IDC_STATUS, szMessage); m_nNextStatusTime = now + nShowTimeMsec; return true; } return false; } void LiveScanClient::SocketThreadFunction() { while (m_bSocketThread) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); HandleSocket(); } } void LiveScanClient::HandleSocket() { char byteToSend; std::lock_guard lock(m_mSocketThreadMutex); if (!m_bConnected) { return; } string received = m_pClientSocket->ReceiveBytes(); for (unsigned int i = 0; i < received.length(); i++) { //capture a frame if (received[i] == MSG_CAPTURE_FRAME) m_bCaptureFrame = true; //calibrate else if (received[i] == MSG_CALIBRATE) m_bCalibrate = true; //receive settings //TODO: what if packet is split? else if (received[i] == MSG_RECEIVE_SETTINGS) { vector bounds(6); i++; int nBytes = *(int*)(received.c_str() + i); i += sizeof(int); for (int j = 0; j < 6; j++) { bounds[j] = *(float*)(received.c_str() + i); i += sizeof(float); } m_bFilter = (received[i]!=0); i++; m_nFilterNeighbors = *(int*)(received.c_str() + i); i += sizeof(int); m_fFilterThreshold = *(float*)(received.c_str() + i); i += sizeof(float); m_vBounds = bounds; int nMarkers = *(int*)(received.c_str() + i); i += sizeof(int); calibration.markerPoses.resize(nMarkers); for (int j = 0; j < nMarkers; j++) { for (int k = 0; k < 3; k++) { for (int l = 0; l < 3; l++) { calibration.markerPoses[j].R[k][l] = *(float*)(received.c_str() + i); i += sizeof(float); } } for (int k = 0; k < 3; k++) { calibration.markerPoses[j].t[k] = *(float*)(received.c_str() + i); i += sizeof(float); } calibration.markerPoses[j].markerId = *(int*)(received.c_str() + i); i += sizeof(int); } m_bStreamOnlyBodies = (received[i] != 0); i++; //so that we do not lose the next character in the stream i--; } //send stored frame else if (received[i] == MSG_REQUEST_STORED_FRAME) { byteToSend = MSG_STORED_FRAME; m_pClientSocket->SendBytes(&byteToSend, 1); if (m_vGatheredRGBPoints.size() > 0) { SendFrame(m_vGatheredVertices[0], m_vGatheredRGBPoints[0], m_vLastFrameBody); m_vGatheredRGBPoints.erase(m_vGatheredRGBPoints.begin(), m_vGatheredRGBPoints.begin() + 1); m_vGatheredVertices.erase(m_vGatheredVertices.begin(), m_vGatheredVertices.begin() + 1); } else { int size = -1; m_pClientSocket->SendBytes((char*)&size, 4); } } //send last frame else if (received[i] == MSG_REQUEST_LAST_FRAME) { byteToSend = MSG_LAST_FRAME; m_pClientSocket->SendBytes(&byteToSend, 1); SendFrame(m_vLastFrameVertices, m_vLastFrameRGB, m_vLastFrameBody); } //receive calibration data else if (received[i] == MSG_RECEIVE_CALIBRATION) { i++; for (int j = 0; j < 3; j++) { for (int k = 0; k < 3; k++) { calibration.worldR[j][k] = *(float*)(received.c_str() + i); i += sizeof(float); } } for (int j = 0; j < 3; j++) { calibration.worldT[j] = *(float*)(received.c_str() + i); i += sizeof(float); } for (int j = 0; j < 3; j++) { for (int k = 0; k < 3; k++) { calibration.cameraR[j][k] = *(float*)(received.c_str() + i); i += sizeof(float); } } for (int j = 0; j < 3; j++) { calibration.cameraT[j] = *(float*)(received.c_str() + i); i += sizeof(float); } //so that we do not lose the next character in the stream i--; } else if (received[i] == MSG_CLEAR_STORED_FRAMES) { m_vGatheredVertices.clear(); m_vGatheredRGBPoints.clear(); } } if (m_bConfirmCaptured) { byteToSend = MSG_CONFIRM_CAPTURED; m_pClientSocket->SendBytes(&byteToSend, 1); m_bConfirmCaptured = false; } if (m_bConfirmCalibrated) { int size = 2 * (9 + 3) * sizeof(float) + sizeof(int) + 1; char *buffer = new char[size]; buffer[0] = MSG_CONFIRM_CALIBRATED; int i = 1; memcpy(buffer + i, &calibration.iUsedMarkerId, 1 * sizeof(int)); i += 1 * sizeof(int); memcpy(buffer + i, calibration.worldR[0].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.worldR[1].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.worldR[2].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.worldT.data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.cameraR[0].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.cameraR[1].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.cameraR[2].data(), 3 * sizeof(float)); i += 3 * sizeof(float); memcpy(buffer + i, calibration.cameraT.data(), 3 * sizeof(float)); m_pClientSocket->SendBytes(buffer, size); m_bConfirmCalibrated = false; } } void LiveScanClient::SendFrame(vector vertices, vector RGB, vector body) { int size = RGB.size() * (3 + 3 * sizeof(short)) + sizeof(int); vector buffer(size); char *ptr2 = (char*)vertices.data(); int pos = 0; int nVertices = RGB.size(); memcpy(buffer.data() + pos, &nVertices, sizeof(nVertices)); pos += sizeof(nVertices); for (unsigned int i = 0; i < RGB.size(); i++) { buffer[pos++] = RGB[i].rgbRed; buffer[pos++] = RGB[i].rgbGreen; buffer[pos++] = RGB[i].rgbBlue; memcpy(buffer.data() + pos, ptr2, sizeof(short)* 3); ptr2 += sizeof(short) * 3; pos += sizeof(short) * 3; } int nBodies = body.size(); size += sizeof(nBodies); for (int i = 0; i < nBodies; i++) { size += sizeof(body[i].bTracked); int nJoints = body[i].vJoints.size(); size += sizeof(nJoints); size += nJoints * (3 * sizeof(float) + 2 * sizeof(int)); size += nJoints * 2 * sizeof(float); } buffer.resize(size); memcpy(buffer.data() + pos, &nBodies, sizeof(nBodies)); pos += sizeof(nBodies); for (int i = 0; i < nBodies; i++) { memcpy(buffer.data() + pos, &body[i].bTracked, sizeof(body[i].bTracked)); pos += sizeof(body[i].bTracked); int nJoints = body[i].vJoints.size(); memcpy(buffer.data() + pos, &nJoints, sizeof(nJoints)); pos += sizeof(nJoints); for (int j = 0; j < nJoints; j++) { //Joint memcpy(buffer.data() + pos, &body[i].vJoints[j].JointType, sizeof(JointType)); pos += sizeof(JointType); memcpy(buffer.data() + pos, &body[i].vJoints[j].TrackingState, sizeof(TrackingState)); pos += sizeof(TrackingState); //Joint position memcpy(buffer.data() + pos, &body[i].vJoints[j].Position.X, sizeof(float)); pos += sizeof(float); memcpy(buffer.data() + pos, &body[i].vJoints[j].Position.Y, sizeof(float)); pos += sizeof(float); memcpy(buffer.data() + pos, &body[i].vJoints[j].Position.Z, sizeof(float)); pos += sizeof(float); //JointInColorSpace memcpy(buffer.data() + pos, &body[i].vJointsInColorSpace[j].X, sizeof(float)); pos += sizeof(float); memcpy(buffer.data() + pos, &body[i].vJointsInColorSpace[j].Y, sizeof(float)); pos += sizeof(float); } } m_pClientSocket->SendBytes((char*)&size, sizeof(int)); m_pClientSocket->SendBytes(buffer.data(), size); } void LiveScanClient::StoreFrame(Point3f *vertices, Point2f *mapping, RGB *color, vector &bodies, BYTE* bodyIndex) { std::vector goodVertices; std::vector goodColorPoints; unsigned int nVertices = pCapture->nDepthFrameWidth * pCapture->nDepthFrameHeight; for (unsigned int vertexIndex = 0; vertexIndex < nVertices; vertexIndex++) { if (m_bStreamOnlyBodies && bodyIndex[vertexIndex] >= bodies.size()) continue; if (vertices[vertexIndex].Z >= 0 && mapping[vertexIndex].Y >= 0 && mapping[vertexIndex].Y < pCapture->nColorFrameHeight) { Point3f temp = vertices[vertexIndex]; RGB tempColor = color[(int)mapping[vertexIndex].X + (int)mapping[vertexIndex].Y * pCapture->nColorFrameWidth]; if (calibration.bCalibrated) { temp.X += calibration.worldT[0]; temp.Y += calibration.worldT[1]; temp.Z += calibration.worldT[2]; temp = RotatePoint(temp, calibration.worldR); if (temp.X < m_vBounds[0] || temp.X > m_vBounds[3] || temp.Y < m_vBounds[1] || temp.Y > m_vBounds[4] || temp.Z < m_vBounds[2] || temp.Z > m_vBounds[5]) continue; } goodVertices.push_back(temp); goodColorPoints.push_back(tempColor); } } for (unsigned int i = 0; i < bodies.size(); i++) { for (unsigned int j = 0; j < bodies[i].vJoints.size(); j++) { if (calibration.bCalibrated) { bodies[i].vJoints[j].Position.X += calibration.worldT[0]; bodies[i].vJoints[j].Position.Y += calibration.worldT[1]; bodies[i].vJoints[j].Position.Z += calibration.worldT[2]; Point3f tempPoint(bodies[i].vJoints[j].Position.X, bodies[i].vJoints[j].Position.Y, bodies[i].vJoints[j].Position.Z); tempPoint = RotatePoint(tempPoint, calibration.worldR); bodies[i].vJoints[j].Position.X = tempPoint.X; bodies[i].vJoints[j].Position.Y = tempPoint.Y; bodies[i].vJoints[j].Position.Z = tempPoint.Z; } } } if (m_bFilter) filter(goodVertices, goodColorPoints, m_nFilterNeighbors, m_fFilterThreshold); vector goodVerticesShort(goodVertices.size()); for (unsigned int i = 0; i < goodVertices.size(); i++) { goodVerticesShort[i] = goodVertices[i]; } m_vLastFrameBody = bodies; m_vLastFrameVertices = goodVerticesShort; m_vLastFrameRGB = goodColorPoints; } void LiveScanClient::ShowFPS() { if (m_hWnd) { double fps = 0.0; LARGE_INTEGER qpcNow = { 0 }; if (m_fFreq) { if (QueryPerformanceCounter(&qpcNow)) { if (m_nLastCounter) { m_nFramesSinceUpdate++; fps = m_fFreq * m_nFramesSinceUpdate / double(qpcNow.QuadPart - m_nLastCounter); } } } WCHAR szStatusMessage[64]; StringCchPrintf(szStatusMessage, _countof(szStatusMessage), L" FPS = %0.2f", fps); if (SetStatusMessage(szStatusMessage, 1000, false)) { m_nLastCounter = qpcNow.QuadPart; m_nFramesSinceUpdate = 0; } } } void LiveScanClient::ReadIPFromFile() { ifstream file; file.open("lastIP.txt"); if (file.is_open()) { char lastUsedIPAddress[20]; file.getline(lastUsedIPAddress, 20); file.close(); SetDlgItemTextA(m_hWnd, IDC_IP, lastUsedIPAddress); } } void LiveScanClient::WriteIPToFile() { ofstream file; file.open("lastIP.txt"); char lastUsedIPAddress[20]; GetDlgItemTextA(m_hWnd, IDC_IP, lastUsedIPAddress, 20); file << lastUsedIPAddress; file.close(); }