diff --git a/include/libcamera/internal/ipa_proxy_raspberrypi.h b/include/libcamera/internal/ipa_proxy_raspberrypi.h
new file mode 100644
index 00000000..bc2001ad
--- /dev/null
+++ b/include/libcamera/internal/ipa_proxy_raspberrypi.h
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * ipa_proxy_raspberrypi.h - Image Processing Algorithm proxy for Raspberry pi
+ */
+#ifndef __LIBCAMERA_INTERNAL_IPA_PROXY_RASPBERRYPI_H__
+#define __LIBCAMERA_INTERNAL_IPA_PROXY_RASPBERRYPI_H__
+
+// automatically generated by custom compiler
+
+// TODO move this to a proxy header directory
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/raspberrypi.h>
+#include <libcamera/ipa/raspberrypi_wrapper.h>
+
+#include "libcamera/internal/ipa_ipc.h"
+#include "libcamera/internal/ipa_proxy.h"
+#include "libcamera/internal/ipc_unixsocket.h"
+#include "libcamera/internal/thread.h"
+
+namespace libcamera {
+
+class IPAProxyRPi : public IPAProxy, public IPARPiInterface
+{
+public:
+	IPAProxyRPi(IPAModule *ipam, bool isolate);
+	~IPAProxyRPi();
+
+	int init(const IPASettings &settings) override;
+	int start() override;
+	void stop() override;
+	void configure(const CameraSensorInfo &sensorInfo,
+		       const std::map<unsigned int, IPAStream> &streamConfig,
+		       const std::map<unsigned int, const ControlInfoMap> &entityControls,
+		       const RPiConfigureParams &ipaConfig,
+		       RPiConfigureParams *result) override;
+	void mapBuffers(const std::vector<IPABuffer> &buffers) override;
+	void unmapBuffers(const std::vector<unsigned int> &ids) override;
+	void processEvent(const RPiEventParams &event) override;
+
+	Signal<unsigned int, const RPiActionParams &> queueFrameAction;
+
+private:
+	void recvIPC(std::vector<uint8_t> &data, std::vector<int32_t> &fds);
+
+	int initThread(const IPASettings &settings);
+	int startThread();
+	void stopThread();
+	void configureThread(const CameraSensorInfo &sensorInfo,
+		       const std::map<unsigned int, IPAStream> &streamConfig,
+		       const std::map<unsigned int, const ControlInfoMap> &entityControls,
+		       const RPiConfigureParams &ipaConfig,
+		       RPiConfigureParams *result);
+	void mapBuffersThread(const std::vector<IPABuffer> &buffers);
+	void unmapBuffersThread(const std::vector<unsigned int> &ids);
+	void processEventThread(const RPiEventParams &event);
+	void frameActionHandlerThread(unsigned int frame, const RPiActionParams &data);
+
+	int initIPC(const IPASettings &settings);
+	int startIPC();
+	void stopIPC();
+	void configureIPC(const CameraSensorInfo &sensorInfo,
+		       const std::map<unsigned int, IPAStream> &streamConfig,
+		       const std::map<unsigned int, const ControlInfoMap> &entityControls,
+		       const RPiConfigureParams &ipaConfig,
+		       RPiConfigureParams *result);
+	void mapBuffersIPC(const std::vector<IPABuffer> &buffers);
+	void unmapBuffersIPC(const std::vector<unsigned int> &ids);
+	void processEventIPC(const RPiEventParams &event);
+	void frameActionHandlerIPC(std::vector<uint8_t> &data,
+				   std::vector<int32_t> &fds);
+
+	/* Helper class to invoke processEvent() in another thread. */
+	class ThreadProxy : public Object
+	{
+	public:
+		void setIPA(IPARPiInterface *ipa)
+		{
+			ipa_ = ipa;
+		}
+
+		int start()
+		{
+			return ipa_->start();
+		}
+
+		void stop()
+		{
+			ipa_->stop();
+		}
+
+		void processEvent(const RPiEventParams &event)
+		{
+			ipa_->processEvent(event);
+		}
+
+	private:
+		IPARPiInterface *ipa_;
+	};
+
+	bool running_;
+	Thread thread_;
+	ThreadProxy proxy_;
+	std::unique_ptr<IPARPiInterface> ipa_;
+
+	const bool isolate_;
+
+	std::unique_ptr<IPAIPC> ipc_;
+
+	ControlSerializer controlSerializer_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_INTERNAL_IPA_PROXY_RASPBERRYPI_H__ */
diff --git a/src/libcamera/proxy/ipa_proxy_raspberrypi.cpp b/src/libcamera/proxy/ipa_proxy_raspberrypi.cpp
new file mode 100644
index 00000000..6526fb1d
--- /dev/null
+++ b/src/libcamera/proxy/ipa_proxy_raspberrypi.cpp
@@ -0,0 +1,381 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * ipa_proxy_raspberrypi.cpp - Image Processing Algorithm proxy for Raspberry pi
+ */
+
+// automatically generated by custom compiler
+
+#include <vector>
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/ipa/raspberrypi.h>
+#include <libcamera/ipa/raspberrypi_wrapper.h>
+#include <libcamera/ipa/raspberrypi_serializer.h>
+
+#include "libcamera/internal/control_serializer.h"
+#include "libcamera/internal/ipa_ipc.h"
+#include "libcamera/internal/ipa_data_serializer.h"
+#include "libcamera/internal/ipa_module.h"
+#include "libcamera/internal/ipa_proxy.h"
+#include "libcamera/internal/ipc_unixsocket.h"
+#include "libcamera/internal/log.h"
+#include "libcamera/internal/process.h"
+#include "libcamera/internal/thread.h"
+
+#include "libcamera/internal/ipa_proxy_raspberrypi.h"
+
+// just inline this at code generation
+#define CALL_THREAD_OR_IPC(isolate, thread_func, ipc_func, ...) \
+	(isolate ? ipc_func(__VA_ARGS__) : thread_func(__VA_ARGS__))
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(IPAProxy)
+
+// TODO yeah we really need to move this into a header somewhere
+void appendUInt32(std::vector<uint8_t> &vec, uint32_t val)
+{
+	uint8_t arr[] = {static_cast<uint8_t>(val & 0xff),
+			 static_cast<uint8_t>((val >> 8) & 0xff),
+			 static_cast<uint8_t>((val >> 16) & 0xff),
+			 static_cast<uint8_t>((val >> 24) & 0xff)};
+	vec.insert(vec.end(), arr, arr + 4);
+}
+
+IPAProxyRPi::IPAProxyRPi(IPAModule *ipam, bool isolate)
+	: IPAProxy(ipam), running_(false),
+	  isolate_(isolate)
+{
+	LOG(IPAProxy, Debug)
+		<< "initializing dummy proxy: loading IPA from "
+		<< ipam->path();
+
+	if (isolate_) {
+		const std::string proxy_worker_path = resolvePath("ipa_proxy_raspberrypi");
+		if (proxy_worker_path.empty()) {
+			LOG(IPAProxy, Error)
+				<< "Failed to get proxy worker path";
+			return;
+		}
+
+		IPAIPCFactory *ipcf = nullptr;
+		for (IPAIPCFactory *factory : IPAIPCFactory::factories()) {
+			if (!strcmp(factory->name().c_str(), "IPAIPCUnixSocket")) {
+				ipcf = factory;
+				break;
+			}
+		}
+
+		if (!ipcf) {
+			LOG(IPAProxy, Error) << "Failed to get IPAIPC factory";
+			return;
+		}
+
+		ipc_ = ipcf->create(ipam->path().c_str(), proxy_worker_path.c_str());
+		if (!ipc_->isValid()) {
+			LOG(IPAProxy, Error) << "Failed to create IPAIPC";
+			return;
+		}
+
+		ipc_->recvIPC.connect(this, &IPAProxyRPi::recvIPC);
+
+		valid_ = true;
+		return;
+	}
+
+	if (!ipam->load())
+		return;
+
+	IPAInterface *ipai = ipam->createInterface();
+	if (!ipai) {
+		LOG(IPAProxy, Error)
+			<< "Failed to create IPA context for " << ipam->path();
+		return;
+	}
+
+	ipa_ = std::unique_ptr<IPARPiInterface>(dynamic_cast<IPARPiInterface *>(ipai));
+	proxy_.setIPA(ipa_.get());
+
+	/*
+	 * Proxy the queueFrameAction signal to dispatch it in the caller's
+	 * thread.
+	 */
+	ipa_->queueFrameAction.connect(this, &IPAProxyRPi::frameActionHandlerThread);
+
+	valid_ = true;
+}
+
+IPAProxyRPi::~IPAProxyRPi()
+{
+	if (isolate_)
+		ipc_->sendAsync(CMD_EXIT, {}, {});
+}
+
+void IPAProxyRPi::recvIPC(std::vector<uint8_t> &data, std::vector<int32_t> &fds)
+{
+	uint32_t cmd = (data[0] & 0xff) |
+		       ((data[1] & 0xff) << 8) |
+		       ((data[2] & 0xff) << 16) |
+		       ((data[3] & 0xff) << 24);
+
+	/* Need to skip another 4 bytes for the sequence number. */
+	std::vector<uint8_t> vec(data.begin() + 8, data.end());
+
+	switch (cmd) {
+	case CMD_QUEUEFRAMEACTION: {
+		frameActionHandlerIPC(vec, fds);
+		break;
+	}
+	}
+}
+
+int IPAProxyRPi::init(const IPASettings &settings)
+{
+	return CALL_THREAD_OR_IPC(isolate_, initThread, initIPC, settings);
+}
+
+int IPAProxyRPi::start()
+{
+	return CALL_THREAD_OR_IPC(isolate_, startThread, startIPC);
+}
+
+void IPAProxyRPi::stop()
+{
+	CALL_THREAD_OR_IPC(isolate_, stopThread, stopIPC);
+}
+
+void IPAProxyRPi::configure(const CameraSensorInfo &sensorInfo,
+			    const std::map<unsigned int, IPAStream> &streamConfig,
+			    const std::map<unsigned int, const ControlInfoMap> &entityControls,
+			    const RPiConfigureParams &ipaConfig,
+			    RPiConfigureParams *result)
+{
+	CALL_THREAD_OR_IPC(isolate_, configureThread, configureIPC,
+			   sensorInfo, streamConfig, entityControls, ipaConfig, result);
+}
+
+void IPAProxyRPi::mapBuffers(const std::vector<IPABuffer> &buffers)
+{
+	CALL_THREAD_OR_IPC(isolate_, mapBuffersThread, mapBuffersIPC, buffers);
+}
+
+void IPAProxyRPi::unmapBuffers(const std::vector<unsigned int> &ids)
+{
+	CALL_THREAD_OR_IPC(isolate_, unmapBuffersThread, unmapBuffersIPC, ids);
+}
+
+void IPAProxyRPi::processEvent(const RPiEventParams &event)
+{
+	CALL_THREAD_OR_IPC(isolate_, processEventThread, processEventIPC, event);
+}
+
+/* Thread functions */
+
+int IPAProxyRPi::initThread(const IPASettings &settings)
+{
+	int ret = ipa_->init(settings);
+	if (ret)
+		return ret;
+
+	proxy_.moveToThread(&thread_);
+
+	return 0;
+}
+
+int IPAProxyRPi::startThread()
+{
+	running_ = true;
+	thread_.start();
+
+	return proxy_.invokeMethod(&ThreadProxy::start, ConnectionTypeBlocking);
+}
+
+void IPAProxyRPi::stopThread()
+{
+	if (!running_)
+		return;
+
+	running_ = false;
+
+	proxy_.invokeMethod(&ThreadProxy::stop, ConnectionTypeBlocking);
+
+	thread_.exit();
+	thread_.wait();
+}
+
+void IPAProxyRPi::configureThread(const CameraSensorInfo &sensorInfo,
+			    const std::map<unsigned int, IPAStream> &streamConfig,
+			    const std::map<unsigned int, const ControlInfoMap> &entityControls,
+			    const RPiConfigureParams &ipaConfig,
+			    RPiConfigureParams *result)
+{
+	ipa_->configure(sensorInfo, streamConfig, entityControls, ipaConfig,
+			result);
+}
+
+void IPAProxyRPi::mapBuffersThread(const std::vector<IPABuffer> &buffers)
+{
+	ipa_->mapBuffers(buffers);
+}
+
+void IPAProxyRPi::unmapBuffersThread(const std::vector<unsigned int> &ids)
+{
+	ipa_->unmapBuffers(ids);
+}
+
+void IPAProxyRPi::processEventThread(const RPiEventParams &event)
+{
+	if (!running_)
+		return;
+
+	/* Dispatch the processEvent() call to the thread. */
+	proxy_.invokeMethod(&ThreadProxy::processEvent, ConnectionTypeQueued,
+			    event);
+}
+
+void IPAProxyRPi::frameActionHandlerThread(unsigned int frame, const RPiActionParams &data)
+{
+	queueFrameAction.emit(frame, data);
+}
+
+/* IPC functions */
+
+int IPAProxyRPi::initIPC(const IPASettings &settings)
+{
+	std::vector<uint8_t> buf;
+	std::tie(buf, std::ignore) = IPADataSerializer<IPASettings>::serialize(settings);
+	std::vector<uint8_t> resultBuf;
+
+	int ret = ipc_->sendSync(CMD_INIT, buf, {}, &resultBuf);
+	if (ret < 0) {
+		LOG(IPAProxy, Error) << "Failed to call init";
+		return ret;
+	}
+	LOG(IPAProxy, Info) << "Done calling init";
+
+	/* Maybe we should cast this properly? */
+	return static_cast<int32_t>(resultBuf[0]);
+}
+
+int IPAProxyRPi::startIPC()
+{
+	std::vector<uint8_t> resultBuf;
+
+	int ret = ipc_->sendSync(CMD_START, {}, {}, &resultBuf);
+	if (ret < 0) {
+		LOG(IPAProxy, Error) << "Failed to call start";
+		return ret;
+	}
+	LOG(IPAProxy, Info) << "Done calling start";
+
+	/* Maybe we should cast this properly? */
+	return static_cast<int32_t>(resultBuf[0]);
+}
+
+void IPAProxyRPi::stopIPC()
+{
+	int ret = ipc_->sendSync(CMD_STOP, {}, {});
+	if (ret < 0)
+		LOG(IPAProxy, Error) << "Failed to call stop";
+	LOG(IPAProxy, Info) << "Done calling stop";
+}
+
+void IPAProxyRPi::configureIPC(const CameraSensorInfo &sensorInfo,
+			       const std::map<unsigned int, IPAStream> &streamConfig,
+			       const std::map<unsigned int, const ControlInfoMap> &entityControls,
+			       const RPiConfigureParams &ipaConfig,
+			       RPiConfigureParams *result)
+{
+	std::vector<uint8_t> sensorInfoBuf;
+	std::tie(sensorInfoBuf, std::ignore) =
+		IPADataSerializer<CameraSensorInfo>::serialize(sensorInfo);
+	std::vector<uint8_t> streamConfigBuf;
+	std::tie(streamConfigBuf, std::ignore) =
+		IPADataSerializer<std::map<unsigned int, IPAStream>>::serialize(streamConfig);
+	std::vector<uint8_t> entityControlsBuf;
+	std::tie(entityControlsBuf, std::ignore) =
+		IPADataSerializer<std::map<unsigned int, const ControlInfoMap>>::serialize(entityControls, &controlSerializer_);
+	std::vector<uint8_t> ipaConfigBuf;
+	std::vector<int32_t> ipaConfigFds;
+	std::tie(ipaConfigBuf, ipaConfigFds) =
+		IPADataSerializer<RPiConfigureParams>::serialize(ipaConfig, &controlSerializer_);
+
+	std::vector<uint8_t> resultBuf;
+	std::vector<int32_t> resultFds;
+
+	std::vector<uint8_t> input;
+	appendUInt32(input, sensorInfoBuf.size());
+	appendUInt32(input, streamConfigBuf.size());
+	appendUInt32(input, entityControlsBuf.size());
+	appendUInt32(input, ipaConfigBuf.size());
+	appendUInt32(input, ipaConfigFds.size());
+	input.insert(input.end(), sensorInfoBuf.begin(), sensorInfoBuf.end());
+	input.insert(input.end(), streamConfigBuf.begin(), streamConfigBuf.end());
+	input.insert(input.end(), entityControlsBuf.begin(), entityControlsBuf.end());
+	input.insert(input.end(), ipaConfigBuf.begin(), ipaConfigBuf.end());
+
+	std::vector<int32_t> fds;
+	fds.insert(fds.end(), ipaConfigFds.begin(), ipaConfigFds.end());
+
+	int ret = ipc_->sendSync(CMD_CONFIGURE, input, fds, &resultBuf, &resultFds);
+	if (ret < 0) {
+		LOG(IPAProxy, Error) << "Failed to call configure";
+		return;
+	}
+	LOG(IPAProxy, Info) << "Done calling configure";
+
+	*result = IPADataSerializer<RPiConfigureParams>::deserialize(resultBuf, resultFds, &controlSerializer_);
+}
+
+void IPAProxyRPi::mapBuffersIPC(const std::vector<IPABuffer> &buffers)
+{
+	std::vector<uint8_t> buffersBuf;
+	std::vector<int32_t> fdsBuf;
+	std::tie(buffersBuf, fdsBuf) =
+		IPADataSerializer<std::vector<IPABuffer>>::serialize(buffers);
+
+	int ret = ipc_->sendSync(CMD_MAPBUFFERS, buffersBuf, fdsBuf);
+	if (ret < 0)
+		LOG(IPAProxy, Error) << "Failed to call mapBuffers";
+	LOG(IPAProxy, Info) << "Done calling mapBuffers";
+}
+
+void IPAProxyRPi::unmapBuffersIPC(const std::vector<unsigned int> &ids)
+{
+	std::vector<uint8_t> idsBuf;
+	std::tie(idsBuf, std::ignore) =
+		IPADataSerializer<std::vector<unsigned int>>::serialize(ids);
+	int ret = ipc_->sendSync(CMD_UNMAPBUFFERS, idsBuf, {});
+	if (ret < 0)
+		LOG(IPAProxy, Error) << "Failed to call unmapBuffers";
+	LOG(IPAProxy, Info) << "Done calling unmapBuffers";
+}
+
+void IPAProxyRPi::processEventIPC(const RPiEventParams &event)
+{
+	std::vector<uint8_t> eventBuf;
+	std::tie(eventBuf, std::ignore) =
+		IPADataSerializer<RPiEventParams>::serialize(event, &controlSerializer_);
+	int ret = ipc_->sendAsync(CMD_PROCESSEVENT, eventBuf, {});
+	if (ret < 0)
+		LOG(IPAProxy, Error) << "Failed to call processEvent";
+	LOG(IPAProxy, Info) << "Done calling processEvent";
+}
+
+void IPAProxyRPi::frameActionHandlerIPC(std::vector<uint8_t> &data,
+					[[maybe_unused]] std::vector<int32_t> &fds)
+{ 
+	uint32_t frame = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
+	RPiActionParams params =
+		IPADataSerializer<RPiActionParams>::deserialize(data.begin() + 4, data.end(), &controlSerializer_);
+
+	queueFrameAction.emit(frame, params);
+}
+
+
+REGISTER_IPA_PROXY(IPAProxyRPi)
+
+} /* namespace libcamera */
diff --git a/src/libcamera/proxy/worker/ipa_proxy_raspberrypi_worker.cpp b/src/libcamera/proxy/worker/ipa_proxy_raspberrypi_worker.cpp
new file mode 100644
index 00000000..ad917756
--- /dev/null
+++ b/src/libcamera/proxy/worker/ipa_proxy_raspberrypi_worker.cpp
@@ -0,0 +1,355 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * ipa_proxy_raspberrypi_worker.cpp - Image Processing Algorithm proxy worker for Raspberry pi
+ */
+
+// automatically generated by custom compiler
+
+#include <algorithm>
+#include <iostream>
+#include <sys/types.h>
+#include <tuple>
+#include <unistd.h>
+
+#include <libcamera/event_dispatcher.h>
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/raspberrypi_wrapper.h>
+#include <libcamera/ipa/raspberrypi_serializer.h>
+#include <libcamera/logging.h>
+
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/control_serializer.h"
+#include "libcamera/internal/ipa_data_serializer.h"
+#include "libcamera/internal/ipa_module.h"
+#include "libcamera/internal/ipc_unixsocket.h"
+#include "libcamera/internal/log.h"
+#include "libcamera/internal/thread.h"
+
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(IPAProxyRPiWorker)
+
+struct CallData {
+	IPCUnixSocket::Payload *response;
+	bool done;
+};
+
+IPARPiInterface *ipa_;
+IPCUnixSocket socket_;
+std::map<uint32_t, struct CallData> callData_;
+
+ControlSerializer controlSerializer_;
+
+bool exit_ = false;
+
+void writeHeader(IPCUnixSocket::Payload &payload, uint32_t cmd, uint32_t seq)
+{
+	uint8_t cmd_arr[] = {static_cast<uint8_t>(cmd & 0xff),
+			     static_cast<uint8_t>((cmd >> 8 ) & 0xff),
+			     static_cast<uint8_t>((cmd >> 16) & 0xff),
+			     static_cast<uint8_t>((cmd >> 24) & 0xff)};
+	uint8_t seq_arr[] = {static_cast<uint8_t>(seq & 0xff),
+			     static_cast<uint8_t>((seq >> 8 ) & 0xff),
+			     static_cast<uint8_t>((seq >> 16) & 0xff),
+			     static_cast<uint8_t>((seq >> 24) & 0xff)};
+	payload.data.insert(payload.data.begin(), cmd_arr, cmd_arr+4);
+	payload.data.insert(payload.data.begin() + 4, seq_arr, seq_arr+4);
+}
+
+// cmd, seq
+std::tuple<uint32_t, uint32_t> readHeader(IPCUnixSocket::Payload &payload)
+{
+	uint32_t cmd = (payload.data[0] & 0xff) |
+		       ((payload.data[1] & 0xff) << 8) |
+		       ((payload.data[2] & 0xff) << 16) |
+		       ((payload.data[3] & 0xff) << 24);
+	uint32_t seq = (payload.data[4] & 0xff) |
+		       ((payload.data[5] & 0xff) << 8) |
+		       ((payload.data[6] & 0xff) << 16) |
+		       ((payload.data[7] & 0xff) << 24);
+
+	return {cmd, seq};
+}
+
+void eraseHeader(IPCUnixSocket::Payload &payload)
+{
+	payload.data.erase(payload.data.begin(), payload.data.begin() + 8);
+}
+
+void writeUInt32(IPCUnixSocket::Payload &payload, uint32_t val, uint32_t pos)
+{
+	if (pos + 4 > payload.data.size())
+		payload.data.resize(pos + 4);
+
+	uint8_t arr[] = {static_cast<uint8_t>(val & 0xff),
+			 static_cast<uint8_t>((val >> 8 ) & 0xff),
+			 static_cast<uint8_t>((val >> 16) & 0xff),
+			 static_cast<uint8_t>((val >> 24) & 0xff)};
+
+	std::copy(arr, arr + 4, payload.data.begin() + pos);
+}
+
+uint32_t readUInt32(IPCUnixSocket::Payload &payload, uint32_t pos)
+{
+	if (pos + 4 > payload.data.size())
+		return 0;
+
+	return (payload.data[pos] & 0xff) |
+	       ((payload.data[pos + 1] & 0xff) << 8) |
+	       ((payload.data[pos + 2] & 0xff) << 16) |
+	       ((payload.data[pos + 3] & 0xff) << 24);
+}
+
+void readyRead(IPCUnixSocket *socket)
+{
+	IPCUnixSocket::Payload message, response;
+	int ret = socket->receive(&message);
+	if (ret) {
+		LOG(IPAProxyRPiWorker, Error)
+			<< "Receive message failed" << ret;
+		return;
+	}
+
+	uint32_t cmd, seq;
+	std::tie(cmd, seq) = readHeader(message);
+	eraseHeader(message);
+
+	switch (cmd) {
+	case CMD_INIT: {
+		IPASettings settings = IPADataSerializer<IPASettings>::deserialize(message.data);
+
+		int ret = ipa_->init(settings);
+		writeHeader(response, cmd, seq);
+		/* Maybe we should do this cast properly? */
+		response.data.push_back(static_cast<uint8_t>(ret));
+
+		ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to init() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to init()";
+		break;
+	}
+
+	case CMD_EXIT: {
+		exit_ = true;
+		break;
+	}
+
+	case CMD_START: {
+		int ret = ipa_->start();
+		writeHeader(response, cmd, seq);
+		/* Maybe we should do this cast properly? */
+		response.data.push_back(static_cast<uint8_t>(ret));
+
+		ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to start() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to start()";
+		break;
+	}
+
+	case CMD_STOP: {
+		ipa_->stop();
+
+		writeHeader(response, cmd, seq);
+		int ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to stop() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to stop()";
+		break;
+	}
+
+	case CMD_CONFIGURE: {
+		LOG(IPAProxyRPiWorker, Info) << "Starting configure()";
+
+		size_t sensorInfoSize = readUInt32(message, 0);
+		size_t streamConfigSize = readUInt32(message, 4);
+		size_t entityControlsSize = readUInt32(message, 8);
+
+		// TODO make this pattern more like the data ones
+		// - this will be fixed in the compiler
+		// remember to fix these offsets too
+		size_t ipaConfigFdsSize = readUInt32(message, 16);
+
+		size_t sensorInfoStart = 20;
+		size_t streamConfigStart = sensorInfoStart + sensorInfoSize;
+		size_t entityControlsStart = streamConfigStart + streamConfigSize;
+		size_t ipaConfigStart = entityControlsStart + entityControlsSize;
+
+		LOG(IPAProxyRPiWorker, Info) << "  deserializing sensorInfo...";
+		struct CameraSensorInfo sensorInfo =
+			IPADataSerializer<struct CameraSensorInfo>::deserialize(
+				message.data.begin() + sensorInfoStart,
+				message.data.begin() + streamConfigStart);
+		LOG(IPAProxyRPiWorker, Info) << "  deserializing streamConfig...";
+		std::map<unsigned int, struct IPAStream> streamConfig =
+			IPADataSerializer<std::map<unsigned int, struct IPAStream>>::deserialize(
+				message.data.begin() + streamConfigStart,
+				message.data.begin() + entityControlsStart);
+		LOG(IPAProxyRPiWorker, Info) << "  deserializing entityControls...";
+		const std::map<unsigned int, const ControlInfoMap> entityControls =
+			IPADataSerializer<std::map<unsigned int, const ControlInfoMap>>::deserialize(
+				message.data.begin() + entityControlsStart,
+				message.data.begin() + ipaConfigStart,
+				&controlSerializer_);
+		LOG(IPAProxyRPiWorker, Info) << "  deserializing ipaConfig...";
+		RPiConfigureParams ipaConfig =
+			IPADataSerializer<RPiConfigureParams>::deserialize(
+				message.data.begin() + ipaConfigStart,
+				message.data.end(),
+				message.fds.begin(),
+				message.fds.begin() + ipaConfigFdsSize,
+				&controlSerializer_);
+		RPiConfigureParams results;
+
+		LOG(IPAProxyRPiWorker, Info) << "Calling IPA's configure()";
+		ipa_->configure(sensorInfo, streamConfig, entityControls, ipaConfig, &results);
+		LOG(IPAProxyRPiWorker, Info) << "Done calling IPA's configure()";
+
+		std::vector<uint8_t> resultsSerialized;
+		std::vector<int32_t> resultsFds;
+		std::tie(resultsSerialized, resultsFds) =
+			IPADataSerializer<RPiConfigureParams>::serialize(results, &controlSerializer_);
+
+		writeHeader(response, cmd, seq);
+		response.data.insert(response.data.end(),
+				     resultsSerialized.begin(), resultsSerialized.end());
+		response.fds.insert(response.fds.end(),
+				    resultsFds.begin(), resultsFds.end());
+		LOG(IPAProxyRPiWorker, Info) << "Sending response to configure()";
+		int ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to configure() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to configure()";
+		break;
+	}
+
+	case CMD_MAPBUFFERS: {
+		std::vector<IPABuffer> ipaBuffers =
+			IPADataSerializer<std::vector<IPABuffer>>::deserialize(message.data, message.fds);
+		ipa_->mapBuffers(ipaBuffers);
+
+		writeHeader(response, cmd, seq);
+		int ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to mapBuffers() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to mapBuffers()";
+		break;
+	}
+
+	case CMD_UNMAPBUFFERS: {
+		std::vector<unsigned int> ids =
+			IPADataSerializer<std::vector<unsigned int>>::deserialize(message.data);
+		ipa_->unmapBuffers(ids);
+
+		writeHeader(response, cmd, seq);
+		int ret = socket_.send(response);
+		if (ret < 0) {
+			LOG(IPAProxyRPiWorker, Error)
+				<< "Reply to unmapBuffers() failed" << ret;
+		}
+
+		LOG(IPAProxyRPiWorker, Info) << "Done replying to unmapBuffers()";
+		break;
+	}
+
+	case CMD_PROCESSEVENT: {
+		RPiEventParams ev =
+			IPADataSerializer<RPiEventParams>::deserialize(message.data, &controlSerializer_);
+		ipa_->processEvent(ev);
+
+		LOG(IPAProxyRPiWorker, Info) << "Done processEvent()ing";
+		break;
+	}
+
+	}
+
+	return;
+}
+
+void queueFrameAction(unsigned int frame, const RPiActionParams &action)
+{
+	LOG(IPAProxyRPiWorker, Info) << "queueFrameAction triggered";
+	IPCUnixSocket::Payload message;
+	writeUInt32(message, frame, 0);
+	std::vector<uint8_t> actionVector;
+	std::tie(actionVector, std::ignore) =
+		IPADataSerializer<RPiActionParams>::serialize(action, &controlSerializer_);
+
+	message.data.insert(message.data.end(), actionVector.begin(), actionVector.end());
+	writeHeader(message, CMD_QUEUEFRAMEACTION, 0);
+
+	socket_.send(message);
+	LOG(IPAProxyRPiWorker, Info) << "queueFrameAction done";
+	return;
+}
+
+int main(int argc, char **argv)
+{
+	/* Uncomment this for debugging. */
+	std::string logPath = "/tmp/libcamera.worker." +
+			      std::to_string(getpid()) + ".log";
+	logSetFile(logPath.c_str());
+	//logSetTarget(LoggingTargetSyslog);
+
+	if (argc < 3) {
+		LOG(IPAProxyRPiWorker, Debug)
+			<< "Tried to start worker with no args";
+		return EXIT_FAILURE;
+	}
+
+	int fd = std::stoi(argv[2]);
+	LOG(IPAProxyRPiWorker, Debug)
+		<< "Starting worker for IPA module " << argv[1]
+		<< " with IPC fd = " << fd;
+
+	std::unique_ptr<IPAModule> ipam = std::make_unique<IPAModule>(argv[1]);
+	if (!ipam->isValid() || !ipam->load()) {
+		LOG(IPAProxyRPiWorker, Error)
+			<< "IPAModule " << argv[1] << " should be valid but isn't";
+		return EXIT_FAILURE;
+	}
+
+	if (socket_.bind(fd) < 0) {
+		LOG(IPAProxyRPiWorker, Error) << "IPC socket binding failed";
+		return EXIT_FAILURE;
+	}
+	socket_.readyRead.connect(&readyRead);
+
+	ipa_ = dynamic_cast<IPARPiInterface *>(ipam->createInterface());
+	if (!ipa_) {
+		LOG(IPAProxyRPiWorker, Error) << "Failed to create IPA context";
+		return EXIT_FAILURE;
+	}
+
+	ipa_->queueFrameAction.connect(&queueFrameAction);
+
+	LOG(IPAProxyRPiWorker, Debug) << "Proxy worker successfully started";
+
+	/* \todo upgrade listening loop */
+	EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+	while (!exit_)
+		dispatcher->processEvents();
+
+	delete ipa_;
+	socket_.close();
+
+	return 0;
+}
