#include "SMXDevice.h" #include "../SMX.h" #include "Helpers.h" #include "SMXDeviceConnection.h" #include "SMXDeviceSearch.h" #include "SMXConfigPacket.h" #include #include #include #include using namespace std; using namespace SMX; // Extract test data for panel iPanel. static void ReadDataForPanel(const vector &data, int iPanel, void *pOut, int iOutSize) { size_t m_iBit = 0; uint8_t *p = (uint8_t *) pOut; // Read each byte. for(int i = 0; i < iOutSize; ++i) { // Read each bit in this byte. uint8_t result = 0; for(int j = 0; j < 8; ++j) { bool bit = false; if(m_iBit < data.size()) { bit = data[m_iBit] & (1 << iPanel); m_iBit++; } result |= bit << j; } *p++ = result; } } shared_ptr SMX::SMXDevice::Create(shared_ptr hEvent, Mutex &lock) { return CreateObj(hEvent, lock); } SMX::SMXDevice::SMXDevice(shared_ptr &pSelf, shared_ptr hEvent, Mutex &lock): m_hEvent(hEvent), m_Lock(lock), m_pSelf(GetPointers(pSelf, this)) { m_pConnection = SMXDeviceConnection::Create(); } SMX::SMXDevice::~SMXDevice() { } bool SMX::SMXDevice::OpenDeviceHandle(shared_ptr pHandle, wstring &sError) { m_Lock.AssertLockedByCurrentThread(); return m_pConnection->Open(pHandle, sError); } void SMX::SMXDevice::CloseDevice() { m_Lock.AssertLockedByCurrentThread(); m_pConnection->Close(); m_bHaveConfig = false; m_bSendConfig = false; m_bSendingConfig = false; m_bWaitingForConfigResponse = false; CallUpdateCallback(SMXUpdateCallback_Updated); } shared_ptr SMX::SMXDevice::GetDeviceHandle() const { return m_pConnection->GetDeviceHandle(); } void SMX::SMXDevice::SetUpdateCallback(function pCallback) { LockMutex Lock(m_Lock); m_pUpdateCallback = pCallback; } bool SMX::SMXDevice::IsConnected() const { m_Lock.AssertNotLockedByCurrentThread(); // Don't expose the device as connected until we've read the current configuration. LockMutex Lock(m_Lock); return IsConnectedLocked(); } bool SMX::SMXDevice::IsConnectedLocked() const { m_Lock.AssertLockedByCurrentThread(); return m_pConnection->IsConnectedWithDeviceInfo() && m_bHaveConfig; } void SMX::SMXDevice::SendCommand(string cmd, function pComplete) { LockMutex Lock(m_Lock); SendCommandLocked(cmd, pComplete); } void SMX::SMXDevice::SendCommandLocked(string cmd, function pComplete) { m_Lock.AssertLockedByCurrentThread(); if(!m_pConnection->IsConnected()) { // If we're not connected, just call pComplete. if(pComplete) pComplete(""); return; } // This call is nonblocking, so it's safe to do this in the UI thread. m_pConnection->SendCommand(cmd, pComplete); // Wake up the communications thread to send the message. if(m_hEvent) SetEvent(m_hEvent->value()); } void SMX::SMXDevice::GetInfo(SMXInfo &info) { LockMutex Lock(m_Lock); GetInfoLocked(info); } void SMX::SMXDevice::GetInfoLocked(SMXInfo &info) { m_Lock.AssertLockedByCurrentThread(); info = SMXInfo(); info.m_bConnected = IsConnectedLocked(); if(!info.m_bConnected) return; // Copy fields from the low-level device info to the high-level struct. // These are kept separate because the interface depends on the format // of SMXInfo, but it doesn't care about anything inside SMXDeviceConnection. SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); memcpy(info.m_Serial, deviceInfo.m_Serial, sizeof(info.m_Serial)); info.m_iFirmwareVersion = deviceInfo.m_iFirmwareVersion; } bool SMX::SMXDevice::IsPlayer2Locked() const { m_Lock.AssertLockedByCurrentThread(); if(!IsConnectedLocked()) return false; return m_pConnection->GetDeviceInfo().m_bP2; } bool SMX::SMXDevice::GetConfig(SMXConfig &configOut) { LockMutex Lock(m_Lock); return GetConfigLocked(configOut); } bool SMX::SMXDevice::GetConfigLocked(SMXConfig &configOut) { m_Lock.AssertLockedByCurrentThread(); // If SetConfig was called to write a new configuration but we haven't sent it // yet, return it instead of the configuration we read alst, so GetConfig // immediately after SetConfig returns the value the caller expects set. if(m_bSendConfig) configOut = wanted_config; else configOut = config; return m_bHaveConfig; } void SMX::SMXDevice::SetConfig(const SMXConfig &newConfig) { LockMutex Lock(m_Lock); wanted_config = newConfig; m_bSendConfig = true; } uint16_t SMX::SMXDevice::GetInputState() const { LockMutex Lock(m_Lock); return m_pConnection->GetInputState(); } void SMX::SMXDevice::FactoryReset() { // Send a factory reset command, and then read the new configuration. LockMutex Lock(m_Lock); SendCommandLocked("f\n"); SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n", [&](string response) { // We now have the new configuration. m_Lock.AssertLockedByCurrentThread(); if(deviceInfo.m_iFirmwareVersion >= 5) { // Factory reset resets the platform strip color saved to the configuration, but doesn't // apply it to the lights. Do that now. string sLightCommand; sLightCommand.push_back('L'); sLightCommand.push_back(0); // LED strip index (always 0) sLightCommand.push_back(44); // number of LEDs to set // config.platformStripColor[0]; for(int i = 0; i < 44; ++i) { sLightCommand += config.platformStripColor[0]; sLightCommand += config.platformStripColor[1]; sLightCommand += config.platformStripColor[2]; } SendCommandLocked(sLightCommand); } CallUpdateCallback(SMXUpdateCallback_FactoryResetCommandComplete); }); } void SMX::SMXDevice::ForceRecalibration() { LockMutex Lock(m_Lock); SendCommandLocked("C\n"); } void SMX::SMXDevice::SetSensorTestMode(SensorTestMode mode) { LockMutex Lock(m_Lock); m_SensorTestMode = mode; } bool SMX::SMXDevice::GetTestData(SMXSensorTestModeData &data) { LockMutex Lock(m_Lock); // Stop if we haven't read test mode data yet. if(!m_HaveSensorTestModeData) return false; data = m_SensorTestData; return true; } void SMX::SMXDevice::CallUpdateCallback(SMXUpdateCallbackReason reason) { m_Lock.AssertLockedByCurrentThread(); if(!m_pUpdateCallback) return; SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); m_pUpdateCallback(deviceInfo.m_bP2? 1:0, reason); } void SMX::SMXDevice::HandlePackets() { m_Lock.AssertLockedByCurrentThread(); while(1) { string buf; if(!m_pConnection->ReadPacket(buf)) break; if(buf.empty()) continue; switch(buf[0]) { case 'y': HandleSensorTestDataResponse(buf); break; // 'g' is sent by firmware versions 1-4. Version 5 and newer send 'G', to ensure // older code doesn't misinterpret the modified config packet format. case 'g': case 'G': { // This command reads back the configuration we wrote with 'w', or the defaults if // we haven't written any. if(buf.size() < 2) { Log("Communication error: invalid configuration packet"); continue; } size_t iSize = (uint8_t) buf[1]; if(buf.size() < iSize+2) { Log("Communication error: invalid configuration packet"); continue; } // Store the raw config data in rawConfig. For V1-4 firmwares, this is the // old config format. rawConfig.resize(iSize); memcpy(rawConfig.data(), buf.data()+2, min(static_cast(iSize), sizeof(config))); if(buf[0] == 'g') { // Convert the old config format to the new one, so the rest of the SDK and // user code doesn't need to deal with multiple formats. ConvertToNewConfig(rawConfig, config); } else { // This is the new config format. Copy it directly into config. memcpy(&config, buf.data()+2, min(iSize, sizeof(config))); } m_bHaveConfig = true; buf.erase(buf.begin(), buf.begin()+iSize+2); // Log(ssprintf("Read back configuration: %i bytes, first byte %i", iSize, buf[2])); CallUpdateCallback(SMXUpdateCallback_Updated); break; } } } } // If m_bSendConfig is true, send the configuration to the pad. Note that while the game // always sends its configuration, so the pad is configured according to the game's configuration, // we only change the configuration if the user changes something so we don't overwrite // his configuration. void SMX::SMXDevice::SendConfig() { m_Lock.AssertLockedByCurrentThread(); if(!m_pConnection->IsConnected() || !m_bSendConfig || m_bSendingConfig) return; // We can't update the configuration until we've received the device's previous // configuration. if(!m_bHaveConfig) return; // If we're still waiting for a previous configuration to read back, don't send // another yet. if(m_bWaitingForConfigResponse) return; // Rate limit updating the configuration, to prevent excess EEPROM wear. This is just // a safeguard in case applications try to change the configuration in realtime. If we've // written the configuration recently, stop. We'll write the most recent configuration // once enough time has passed. This is hidden to the application, since GetConfig returns // wanted_config if it's set. // const float fTimeBetweenConfigUpdates = 1.0f; double fNow = SMX::GetMonotonicTime(); if(m_fDelayConfigUpdatesUntil > fNow) return; m_fDelayConfigUpdatesUntil = fNow + 1.0f; SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); // Write configuration command. This is "w" in versions 1-4, and "W" in versions 5 // and newer. string sData = ssprintf(deviceInfo.m_iFirmwareVersion >= 5? "W":"w"); // Append the config packet. if(deviceInfo.m_iFirmwareVersion < 5) { // Convert wanted_config to the old configuration format. vector outputConfig = rawConfig; ConvertToOldConfig(wanted_config, outputConfig); uint8_t iSize = (uint8_t) outputConfig.size(); sData.append((char *) &iSize, sizeof(iSize)); sData.append((char *) outputConfig.data(), outputConfig.size()); } else { int8_t iSize = sizeof(SMXConfig); sData.append((char *) &iSize, sizeof(iSize)); sData.append((char *) &wanted_config, sizeof(wanted_config)); } // Don't send another config packet until this one finishes, so if we get a bunch of // SetConfig calls quickly we won't spam the device, which can get slow. m_bSendingConfig = true; SendCommandLocked(sData, [&](string response) { m_bSendingConfig = false; }); m_bSendConfig = false; // Assume the configuration is what we just sent, so calls to GetConfig will // continue to return it. Otherwise, they'd return the old values until the // command below completes. config = wanted_config; // Don't send another configuration packet until we receive the response to the above // command. If we're sending updates quickly (eg. dragging the color slider), we can // send multiple updates before we get a response. m_bWaitingForConfigResponse = true; // After we write the configuration, read back the updated configuration to // verify it. This command is "g" in versions 1-4, and "G" in versions 5 and // newer. SendCommandLocked( deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n", [this](string response) { m_bWaitingForConfigResponse = false; }); } void SMX::SMXDevice::Update(wstring &sError) { m_Lock.AssertLockedByCurrentThread(); if(!m_pConnection->IsConnected()) return; CheckActive(); SendConfig(); UpdateSensorTestMode(); { uint16_t iOldState = m_pConnection->GetInputState(); // Process any received packets, and start sending any waiting packets. m_pConnection->Update(sError); if(!sError.empty()) return; // If the inputs changed from packets we just processed, call the update callback. if(iOldState != m_pConnection->GetInputState()) CallUpdateCallback(SMXUpdateCallback_Updated); } HandlePackets(); } void SMX::SMXDevice::CheckActive() { m_Lock.AssertLockedByCurrentThread(); // If there's no connected device, or we've already activated it, we have nothing to do. if(!m_pConnection->IsConnectedWithDeviceInfo() || m_pConnection->GetActive()) return; m_pConnection->SetActive(true); SMXDeviceInfo deviceInfo = m_pConnection->GetDeviceInfo(); // Read the current configuration. The device will return a "g" or "G" response // containing its current SMXConfig. SendCommandLocked(deviceInfo.m_iFirmwareVersion >= 5? "G":"g\n"); } // Check if we need to request test mode data. void SMX::SMXDevice::UpdateSensorTestMode() { m_Lock.AssertLockedByCurrentThread(); if(m_SensorTestMode == SensorTestMode_Off) return; // Request sensor data from the master. Don't send this if we have a request outstanding // already. uint32_t now = GetTickCount(); if(m_WaitingForSensorTestModeResponse != SensorTestMode_Off) { // This request should be quick. If we haven't received a response in a long // time, assume the request wasn't received. if(now - m_SentSensorTestModeRequestAtTicks < 2000) return; } // Send the request. m_WaitingForSensorTestModeResponse = m_SensorTestMode; m_SentSensorTestModeRequestAtTicks = now; SendCommandLocked(ssprintf("y%c\n", m_SensorTestMode)); } // Handle a response to UpdateTestMode. void SMX::SMXDevice::HandleSensorTestDataResponse(const string &sReadBuffer) { m_Lock.AssertLockedByCurrentThread(); // "y" is a response to our "y" query. This is binary data, with the format: // yAB...... // where A is our original query mode (currently '0' or '1'), and B is the number // of bits from each panel in the response. Each bit is encoded as a 16-bit int, // with each int having the response bits from each panel. if(sReadBuffer.size() < 3) return; // If we don't have the whole packet yet, wait. size_t iSize = sReadBuffer[2] * 2; if(sReadBuffer.size() < iSize + 3) return; SensorTestMode iMode = (SensorTestMode) sReadBuffer[1]; // Copy off the data and remove it from the serial buffer. vector data; for(size_t i = 3; i < iSize + 3; i += 2) { uint16_t iValue = (uint8_t(sReadBuffer[i+1]) << 8) | (uint8_t(sReadBuffer[i+0]) << 0); data.push_back(iValue); } if(m_WaitingForSensorTestModeResponse == SensorTestMode_Off) { Log("Ignoring unexpected sensor data request. It may have been sent by another application."); return; } if(iMode != m_WaitingForSensorTestModeResponse) { Log(ssprintf("Ignoring unexpected sensor data request (got %i, expected %i)", iMode, m_WaitingForSensorTestModeResponse)); return; } m_WaitingForSensorTestModeResponse = SensorTestMode_Off; // We match m_WaitingForSensorTestModeResponse, which is the sensor request we most // recently sent. If we don't match g_SensorTestMode, then the sensor mode was changed // while a request was in the air. Just ignore the response. if(iMode != m_SensorTestMode) return; #pragma pack(push,1) struct detail_data { uint8_t sig1:1; // always 0 uint8_t sig2:1; // always 1 uint8_t sig3:1; // always 0 uint8_t bad_sensor_0:1; uint8_t bad_sensor_1:1; uint8_t bad_sensor_2:1; uint8_t bad_sensor_3:1; uint8_t dummy:1; int16_t sensors[4]; uint8_t dip:4; uint8_t bad_sensor_dip_0:1; uint8_t bad_sensor_dip_1:1; uint8_t bad_sensor_dip_2:1; uint8_t bad_sensor_dip_3:1; }; #pragma pack(pop) m_HaveSensorTestModeData = true; SMXSensorTestModeData &output = m_SensorTestData; bool bLastHaveDataFromPanel[9]; memcpy(bLastHaveDataFromPanel, output.bHaveDataFromPanel, sizeof(output.bHaveDataFromPanel)); memset(output.bHaveDataFromPanel, 0, sizeof(output.bHaveDataFromPanel)); memset(output.sensorLevel, 0, sizeof(output.sensorLevel)); memset(output.bBadSensorInput, 0, sizeof(output.bBadSensorInput)); memset(output.iDIPSwitchPerPanel, 0, sizeof(output.iDIPSwitchPerPanel)); memset(output.iBadJumper, 0, sizeof(output.iBadJumper)); for(int iPanel = 0; iPanel < 9; ++iPanel) { // Decode the response from this panel. detail_data pad_data; ReadDataForPanel(data, iPanel, &pad_data, sizeof(pad_data)); // Check the header. This is always 0 1 0, to identify it as a response, and not as random // steps from the player. if(pad_data.sig1 != 0 || pad_data.sig2 != 1 || pad_data.sig3 != 0) { if(bLastHaveDataFromPanel[iPanel]) Log(ssprintf("No data from panel %i (%02x %02x %02x)", iPanel, pad_data.sig1, pad_data.sig2, pad_data.sig3)); output.bHaveDataFromPanel[iPanel] = false; continue; } output.bHaveDataFromPanel[iPanel] = true; // These bits are true if that sensor's most recent reading is invalid. output.bBadSensorInput[iPanel][0] = pad_data.bad_sensor_0; output.bBadSensorInput[iPanel][1] = pad_data.bad_sensor_1; output.bBadSensorInput[iPanel][2] = pad_data.bad_sensor_2; output.bBadSensorInput[iPanel][3] = pad_data.bad_sensor_3; output.iDIPSwitchPerPanel[iPanel] = pad_data.dip; output.iBadJumper[iPanel][0] = pad_data.bad_sensor_dip_0; output.iBadJumper[iPanel][1] = pad_data.bad_sensor_dip_1; output.iBadJumper[iPanel][2] = pad_data.bad_sensor_dip_2; output.iBadJumper[iPanel][3] = pad_data.bad_sensor_dip_3; for(int iSensor = 0; iSensor < 4; ++iSensor) output.sensorLevel[iPanel][iSensor] = pad_data.sensors[iSensor]; } CallUpdateCallback(SMXUpdateCallback_Updated); }