Introduction: Magic Button 4k: the 20USD BMPCC 4k (or 6k) Wireless Remote Control

Many people have asked me to share some details about my wireless controller for the BMPCC4k. Most questions were about the bluetooth control, so I'll mention a few details about that. I am assuming you are familiar with the ESP32 Arduino environment.

This version of the remote can control the recording, focus and aperture of the camera via bluetooth. Have a look at the video. It's quite easy to add more control functions as per the bluetooth control manual of the BMPCC4k. Basically anything in the camera can be controlled, as far as I've seen.

It would be an easy step to add a LIDAR module to measure the distance of a subject, so you can get some kind of an autofocus system... Though it's questionable if you can get an accurate enough focus onto specific areas such as eyes etc...

UPDATE 2020: I made version 3.0. It's based on a free rotating wheel using a magnetic encoder. It also connects to my follow focus motor, which basically becomes a second bluetooth device (the ESP32 supports multiple bluetooth connections). The new video demonstrates this.

If you would like to order version 3, please have a look on the MagicButton website

Supplies

Any ESP32 module with wifi and bluetooth. I used the TTGO micro32 because it's tiny:
https://www.banggood.com/LILYGO-TTGO-Micro-32-V2_0...

A focus wheel, any potentiometer would do. I used the following because it's tiny:
https://www.aliexpress.com/item/32963061806.html?s...
This kind has hard stops at the upper and lower boundary. In a future version I'll use a rotary encoder. This way the focus or aperture doesn't "jump" to the current wheel setting when I enter a mode.

A rec/mode button. I used the following:
https://www.aliexpress.com/item/32806223591.html?s...

Other standard components such as resistors, caps, ... (see schematic)

Step 1: The Code

I use the wifi capability of the ESP32 to either connect to a known network in AP mode, or, when I'm in the field, it becomes a station (STA) to which I can connect to. That way I can configure the module. I won't go into detail of the wifi/webpage section, I might add this at a later stage.

The ESP32 connects to the camera and becomes a Bluetooth LE client. The bluetooth code included in Arduino's ESP32 framework doesn't work with the BMPCC4k. Wakwak-koba has fixed it for us. Thank you Wakwak-koba! I used the BLE library from here:

https://github.com/wakwak-koba/arduino-esp32

Nevertheless that version of the BLE lib is still under development and the latest version of BLEUUID.cpp doesn't seem to work at this moment, so take the earlier "verified" version of this file.

For the rest, most of my bluetooth code is a lot as per the BLE examples included in the Arduino framework:

Some BLE UUID and variable defines:

static BLEUUID BlackMagic("00001800-0000-1000-8000-00805f9b34fb");
static BLEUUID ControlserviceUUID("291D567A-6D75-11E6-8B77-86F30CA893D3");
static BLEUUID DevInfoServiceControlUUID("180A");
static BLEUUID ControlcharUUID("5DD3465F-1AEE-4299-8493-D2ECA2F8E1BB");
static BLEUUID NotifcharUUID("B864E140-76A0-416A-BF30-5876504537D9");
static BLEUUID ClientNamecharUUID("FFAC0C52-C9FB-41A0-B063-CC76282EB89C");
static BLEUUID CamModelcharUUID("2A24");
static BLEScan *pBLEScan = BLEDevice::getScan();
static BLEAddress *pServerAddress;
static BLEAdvertisedDevice* myDevice;
static BLERemoteCharacteristic *pControlCharacteristic;
static BLERemoteCharacteristic *pNotifCharacteristic;
static boolean doConnect =0;
static boolean connected =0;
volatilebool scanning =0;
volatileuint32_t pinCode;

The scanning and main loop:

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks{
	void onResult(BLEAdvertisedDevice advertisedDevice)
	{
		Serial.print("BLE Advertised Device found: ");
		Serial.println(advertisedDevice.toString().c_str());
		if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().equals(BlackMagic))
		{
			Serial.print("Found our device!");
			advertisedDevice.getScan()->stop();
			myDevice = new BLEAdvertisedDevice(advertisedDevice);
			doConnect =true;
		} 
	} 
};    


static void scanCompleteCB(BLEScanResults scanResults)
{
	Serial.println("scanning done");
	scanning =false;
}


void loop(void)
{
	if (!connected && ((uint32_t)(millis() - Timer) > BLE_RESCAN_TIME || (!scanning)))
	{
		Serial.println("scanning...");
		scanning =true;
		pBLEScan->start(BLE_SCAN_TIME, scanCompleteCB);
		Timer = millis();
	}


	if (doConnect ==true)
	{
		if (connectToServer())
		{
			Serial.println("We are now connected to the BLE Server.");
			connected =true;
		}
		else
		{
			Serial.println("We have failed to connect to the server; there is nothin more we will do.");
		}
		doConnect =false;
	}
}

Connecting to the camera:

bool connectToServer(){
	Serial.print("Forming a connection to ");
	Serial.println(myDevice->getAddress().toString().c_str());
	BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT);
	BLEDevice::setSecurityCallbacks(new MySecurity());


	BLESecurity *pSecurity = new BLESecurity();
	pSecurity->setKeySize();
	pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
	pSecurity->setCapability(ESP_IO_CAP_IN);
	pSecurity->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);


	BLEClient *pClient = BLEDevice::createClient();


	pClient->setClientCallbacks(new MyClientCallback());


	pClient->connect(myDevice);
	Serial.println(" - Connected to server");


	BLEDevice::setMTU(BLEDevice::getMTU());


	// OBTAIN CAMERA MODEL
	BLERemoteService *pRemoteService = pClient->getService(DevInfoServiceControlUUID);
	if (pRemoteService == nullptr)
	{
		Serial.print(" - Failed to get device info service");
		Serial.println(DevInfoServiceControlUUID.toString().c_str());
		goto fail;
	}
	Serial.println(" - Reading device info");


	// Obtain a reference to the characteristic in the service of the remote BLE server.
	BLERemoteCharacteristic *pRemoteCamModelCharacteristic = pRemoteService->getCharacteristic(CamModelcharUUID);
	if (pRemoteCamModelCharacteristic == nullptr)
	{
		Serial.print(" - Failed to find camera model");
		Serial.println(CamModelcharUUID.toString().c_str());
		goto fail;
	}


	// Read the value of the characteristic.
	std::string value = pRemoteCamModelCharacteristic->readValue();
	Serial.print("Camera is ");
	Serial.println(value.c_str());
	if (CamModel != value.c_str())
	{
		Serial.print(" - Camera is not BMPCC4k");
		goto fail;
	}


	// OBTAIN CONTROL
	pRemoteService = pClient->getService(ControlserviceUUID);
	if (pRemoteService == nullptr)
	{
		Serial.print(" - Failed to get camera service");
		Serial.println(ControlserviceUUID.toString().c_str());
		goto fail;
	}
	
	BLERemoteCharacteristic *pRemoteClientNameCharacteristic = pRemoteService->getCharacteristic(ClientNamecharUUID);
	if (pRemoteClientNameCharacteristic != nullptr)
	{
		pRemoteClientNameCharacteristic->writeValue(MyName.c_str(), MyName.length());
	}


	pControlCharacteristic = pRemoteService->getCharacteristic(ControlcharUUID);
	if (pControlCharacteristic == nullptr)
	{
		Serial.print(" - Failed to get control characteristic");
		Serial.println(ControlcharUUID.toString().c_str());
		goto fail;
	}
	
	pNotifCharacteristic = pRemoteService->getCharacteristic(NotifcharUUID);
	if (pNotifCharacteristic != nullptr) // && pNotifCharacteristic->canIndicate())
	{
		Serial.println(" - subscribing to notification");
		const uint8_t indicationOn[] = {0x2, 0x0};
		pNotifCharacteristic->registerForNotify(notifyCallback, false);
		pNotifCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn, 2, true);
	}


	return true;


fail:
	pClient->disconnect();
	return false;
}

The connected/disconnected callback:

class MyClientCallback : public BLEClientCallbacks{
	void onConnect(BLEClient *pclient)
	{
		Serial.println("We are connected.");
	}


	void onDisconnect(BLEClient *pclient)
	{
		connected =false;
		pclient->disconnect();
		Serial.println("We got disconnected.");
	}
};

The pin code part:

In my current version I can enter the pincode via the web interface but these are wifi/webpage details which I might add later.

class MySecurity : public BLESecurityCallbacks
{
	uint32_t onPassKeyRequest()
	{
		Serial.println("---> PLEASE ENTER 6 DIGIT PIN (end with ENTER) : ");
		pinCode =0;
		char ch;
		do
		{
			while (!Serial.available())
			{
				delay(1);
			}
			ch = Serial.read();
			if (ch >='0'&& ch <='9')
			{
				pinCode = pinCode *10+ (ch -'0');
				Serial.print(ch);
			}
		}
		while ((ch !='\n'));
		return pinCode;
	}


	void onPassKeyNotify(uint32_t pass_key)
	{
		ESP_LOGE(LOG_TAG, "The passkey Notify number:%d", pass_key);
	}
  
	bool onConfirmPIN(uint32_t pass_key)
	{
		ESP_LOGI(LOG_TAG, "The passkey YES/NO number:%d", pass_key);
		vTaskDelay(5000);
		returntrue;
	}
  
	bool onSecurityRequest()
	{
		ESP_LOGI(LOG_TAG, "Security Request");
		returntrue;
	}
  
	void onAuthenticationComplete(esp_ble_auth_cmpl_t auth_cmpl)
	{
		Serial.print("pair status = ");
		Serial.println(auth_cmpl.success);
	}
};

BLE notification:

The camera notifies its BLE clients about any camera changes, including when the camera starts and stops recording. This code toggles my LED when it starts/stops recording.

static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic,
	uint8_t*pData,
	size_t length,
	bool isNotify)
{
// BMPCC4k BLE message format:
// rec on is 255 9 0 0 10 1 1 2 2 0 64 0 2
// rec off is 255 9 0 0 10 1 1 2 0 0 64 0 2

if (length ==13&& pData[0] ==255&& pData[1] ==9&& pData[4] ==10&& pData[5] ==1) { if (pData[8] ==0) { recstatus =0; } if (pData[8] ==2) { recstatus =1; } } }

Step 2: The Code Part 2

This is the part which actually sends the commands to the camera.

Recording:

uint8_t record[] =   {255, 9, 0, 0, 10, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 0=OFF, 2=ON, [8]
void Record(boolean RecOn) { if (!RecOn) record[8] =0; else record[8] =2; pControlCharacteristic->writeValue((uint8_t*)record, 16, true); }

Focusing:

The camera expects an 11 bit number, ranging from near to far focus. I do advise to put a filter on your ADC value, otherwise the focus might be nervously jittering.

uint8_t focus[] =    {255, 6, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0}; // 0.0 ... 1.0, 11bit, [8]=LSB, [9]=MSB
void Focus(uint16_t val) { //going from a 12bit ADC value to 11bit focus value focus[8] = (uint8_t)(((val >>1) &0xFF)); focus[9] = (uint8_t)(((val >>1) &0xFF00) >>8); pControlCharacteristic->writeValue((uint8_t*)focus, 12, true); }

Aperture:

The camera expects an 11 bit number, ranging from a low to a high aperture value. I do advise to put a filter on your ADC value, otherwise the aperture value might be nervously jittering.

uint8_t aperture[] = {255, 6, 0, 0, 0, 3, 128, 0, 0, 0, 0, 0}; // 0.0 ... 1.0, [8]=LSB, [9]=MSB
void Aperture(uint16_t val) { //going from a 12bit ADC value to 11bit aperture value aperture[8] = (uint8_t)(((val >>1) &0xFF)); aperture[9] = (uint8_t)(((val >>1) &0xFF00) >>8); pControlCharacteristic->writeValue((uint8_t*)aperture, 12, true); }

Step 3: The Circuit

I've attached the PDF of my circuit. Some pics of the PCB are also attached.

The board is powered with micro USB.

After receiving the PCB I decided that I wanted to drive an RGB LED, so I connected two WS2812B in series to the "Button Led" output (that needed some wire patches on the PCB). The PCB's were 8USD with OSHPark.com.

You can see some more connections on the PCB such as "adc" which I'm not using and which were removed from the attached schematics. The plan was to use an external focus wheel in the past but I'm currently perfectly happy with the little thumb wheel.

Step 4: Conclusion

I hope this helped.

I have got some future updates in mind, such as using a rotary encoder without hard stops. This will require the controller to get the current value of the focus or aperture from the camera, and continue from there. The "notifyCallback" function needs to be updated for that probably.

The PCB needs an update to provide the signals for the WS2812B RGB LEDs properly.

I spent a lot (a loooot) of time in making this work, especially the BLE part. If this helped you out and you wanna buy me a drink, that's very much appreciated :) This is a Paypal donation link: