New P4RuntimeClient implementation that supports batching and error reporting

The new client API supports batching and provides detailed response for
write requests (e.g. if entity already exists when inserting), which was
not possible with the old one.

This patch includes:
- New more efficient implementation of P4RuntimeClient (no more locking,
use native gRPC executor, use stub deadlines)
- Ported all codecs to new AbstractCodec-based implementation (needed to
implement codec cache in the future)
- Uses batching in P4RuntimeFlowRuleProgrammable and
P4RuntimeGroupActionProgrammable
- Minor changes to PI framework runtime classes

Change-Id: I3fac42057bb4e1389d761006a32600c786598683
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
index 77a6dc3..fd3b504 100644
--- a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
@@ -38,13 +38,13 @@
 import org.onosproject.net.packet.OutboundPacket;
 import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionParamId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
 import org.onosproject.net.pi.model.PiMatchFieldId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.model.PiTableId;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiControlMetadata;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 
 import java.nio.ByteBuffer;
@@ -211,7 +211,7 @@
     }
 
     @Override
-    public InboundPacket mapInboundPacket(PiPacketOperation packetIn)
+    public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId)
             throws PiInterpreterException {
         // We assume that the packet is ethernet, which is fine since mytunnel.p4
         // can deparse only ethernet packets.
@@ -225,39 +225,38 @@
         }
 
         // Returns the ingress port packet metadata.
-        Optional<PiControlMetadata> packetMetadata = packetIn.metadatas().stream()
+        Optional<PiPacketMetadata> packetMetadata = packetIn.metadatas().stream()
                 .filter(metadata -> metadata.id().toString().equals(INGRESS_PORT))
                 .findFirst();
 
         if (packetMetadata.isPresent()) {
             short s = packetMetadata.get().value().asReadOnlyBuffer().getShort();
             ConnectPoint receivedFrom = new ConnectPoint(
-                    packetIn.deviceId(), PortNumber.portNumber(s));
+                    deviceId, PortNumber.portNumber(s));
             return new DefaultInboundPacket(
                     receivedFrom, ethPkt, packetIn.data().asReadOnlyBuffer());
         } else {
             throw new PiInterpreterException(format(
                     "Missing metadata '%s' in packet-in received from '%s': %s",
-                    INGRESS_PORT, packetIn.deviceId(), packetIn));
+                    INGRESS_PORT, deviceId, packetIn));
         }
     }
 
     private PiPacketOperation createPiPacketOp(ByteBuffer data, long portNumber)
             throws PiInterpreterException {
-        PiControlMetadata metadata = createControlMetadata(portNumber);
+        PiPacketMetadata metadata = createPacketMetadata(portNumber);
         return PiPacketOperation.builder()
-                .forDevice(this.data().deviceId())
                 .withType(PACKET_OUT)
                 .withData(copyFrom(data))
                 .withMetadatas(ImmutableList.of(metadata))
                 .build();
     }
 
-    private PiControlMetadata createControlMetadata(long portNumber)
+    private PiPacketMetadata createPacketMetadata(long portNumber)
             throws PiInterpreterException {
         try {
-            return PiControlMetadata.builder()
-                    .withId(PiControlMetadataId.of(EGRESS_PORT))
+            return PiPacketMetadata.builder()
+                    .withId(PiPacketMetadataId.of(EGRESS_PORT))
                     .withValue(copyFrom(portNumber).fit(PORT_FIELD_BITWIDTH))
                     .build();
         } catch (ImmutableByteSequence.ByteSequenceTrimException e) {
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java
index 8043ae7..48e7b13 100644
--- a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java
@@ -27,6 +27,7 @@
 import org.onosproject.net.pi.model.PiCounterId;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.service.PiPipeconfService;
 import org.onosproject.p4runtime.api.P4RuntimeClient;
@@ -38,7 +39,6 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 import static org.onosproject.net.pi.model.PiCounterType.INDIRECT;
@@ -93,16 +93,14 @@
             counterCellIds.add(PiCounterCellId.ofIndirect(INGRESS_COUNTER_ID, p));
             counterCellIds.add(PiCounterCellId.ofIndirect(EGRESS_COUNTER_ID, p));
         });
+        Set<PiCounterCellHandle> counterCellHandles = counterCellIds.stream()
+                .map(id -> PiCounterCellHandle.of(deviceId, id))
+                .collect(Collectors.toSet());
 
         // Query the device.
-        Collection<PiCounterCell> counterEntryResponse;
-        try {
-            counterEntryResponse = client.readCounterCells(counterCellIds, pipeconf).get();
-        } catch (InterruptedException | ExecutionException e) {
-            log.warn("Exception while reading port counters from {}: {}", deviceId, e.toString());
-            log.debug("", e);
-            return Collections.emptyList();
-        }
+        Collection<PiCounterCell> counterEntryResponse = client.read(pipeconf)
+                    .handles(counterCellHandles).submitSync()
+                    .all(PiCounterCell.class);
 
         // Process response.
         counterEntryResponse.forEach(counterCell -> {
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataId.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataId.java
similarity index 68%
rename from core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataId.java
rename to core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataId.java
index b17c766..53cf1b6 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataId.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataId.java
@@ -23,24 +23,24 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Identifier of a control metadata in a protocol-independent pipeline, unique within the scope of a pipeline model.
+ * Identifier of a packet metadata in a protocol-independent pipeline, unique within the scope of a pipeline model.
  */
 @Beta
-public final class PiControlMetadataId extends Identifier<String> {
+public final class PiPacketMetadataId extends Identifier<String> {
 
-    private PiControlMetadataId(String name) {
+    private PiPacketMetadataId(String name) {
         super(name);
     }
 
     /**
-     * Returns an identifier for the given control metadata name.
+     * Returns an identifier for the given packet metadata name.
      *
-     * @param name control metadata name
-     * @return control metadata ID
+     * @param name packet metadata name
+     * @return packet metadata ID
      */
-    public static PiControlMetadataId of(String name) {
+    public static PiPacketMetadataId of(String name) {
         checkNotNull(name);
         checkArgument(!name.isEmpty(), "Name can't be empty");
-        return new PiControlMetadataId(name);
+        return new PiPacketMetadataId(name);
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataModel.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataModel.java
similarity index 82%
rename from core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataModel.java
rename to core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataModel.java
index 3748eac..76fdbe4 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiControlMetadataModel.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketMetadataModel.java
@@ -19,17 +19,17 @@
 import com.google.common.annotations.Beta;
 
 /**
- * Model of a control metadata for a protocol-independent pipeline.
+ * Model of a packet metadata for a protocol-independent pipeline.
  */
 @Beta
-public interface PiControlMetadataModel {
+public interface PiPacketMetadataModel {
 
     /**
-     * Returns the ID of this control metadata.
+     * Returns the ID of this packet metadata.
      *
      * @return packet operation metadata ID
      */
-    PiControlMetadataId id();
+    PiPacketMetadataId id();
 
     /**
      * Returns the size in bits of this metadata.
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketOperationModel.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketOperationModel.java
index 70659a6..c48ab6b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketOperationModel.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPacketOperationModel.java
@@ -34,11 +34,11 @@
     PiPacketOperationType type();
 
     /**
-     * Returns a list of control metadata models for this packet operation. The metadata models are returned in the same
+     * Returns a list of packet metadata models for this packet operation. The metadata models are returned in the same
      * order as they would appear on the control header that is prepended to the packet.
      *
      * @return list of packet operation metadata models
      */
-    List<PiControlMetadataModel> metadatas();
+    List<PiPacketMetadataModel> metadatas();
 
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
index cada0f9..1d1301e 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
@@ -17,6 +17,7 @@
 package org.onosproject.net.pi.model;
 
 import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.driver.HandlerBehaviour;
 import org.onosproject.net.flow.TrafficTreatment;
@@ -108,14 +109,16 @@
             throws PiInterpreterException;
 
     /**
-     * Returns an inbound packet equivalent to the given PI packet operation.
+     * Returns an inbound packet equivalent to the given PI packet-in operation
+     * for the given device.
      *
      * @param packetOperation packet operation
+     * @param deviceId        ID of the device that originated the packet-in
      * @return inbound packet
      * @throws PiInterpreterException if the packet operation cannot be mapped
      *                                to an inbound packet
      */
-    InboundPacket mapInboundPacket(PiPacketOperation packetOperation)
+    InboundPacket mapInboundPacket(PiPacketOperation packetOperation, DeviceId deviceId)
             throws PiInterpreterException;
 
     /**
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
index f86e70c..57e65a2 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiActionProfileId;
 
 import java.util.Collection;
@@ -143,6 +144,11 @@
         return PiEntityType.ACTION_PROFILE_GROUP;
     }
 
+    @Override
+    public PiActionProfileGroupHandle handle(DeviceId deviceId) {
+        return PiActionProfileGroupHandle.of(deviceId, this);
+    }
+
     /**
      * Builder of action profile groups.
      */
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
index 519a1c4..3579054 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
@@ -27,7 +27,7 @@
  * defined by a device ID, action profile ID and group ID.
  */
 @Beta
-public final class PiActionProfileGroupHandle extends PiHandle<PiActionProfileGroup> {
+public final class PiActionProfileGroupHandle extends PiHandle {
 
     private final PiActionProfileId actionProfileId;
     private final PiActionProfileGroupId groupId;
@@ -39,10 +39,11 @@
     }
 
     /**
-     * Creates a new handle for the given device ID and PI action profile group.
+     * Creates a new handle for the given device ID and PI action profile
+     * group.
      *
      * @param deviceId device ID
-     * @param group PI action profile group
+     * @param group    PI action profile group
      * @return PI action profile group handle
      */
     public static PiActionProfileGroupHandle of(DeviceId deviceId,
@@ -50,6 +51,24 @@
         return new PiActionProfileGroupHandle(deviceId, group);
     }
 
+    /**
+     * Returns the action profile ID of this handle.
+     *
+     * @return action profile ID
+     */
+    public PiActionProfileId actionProfile() {
+        return actionProfileId;
+    }
+
+    /**
+     * Returns the group ID of this handle.
+     *
+     * @return group ID
+     */
+    public PiActionProfileGroupId groupId() {
+        return groupId;
+    }
+
     @Override
     public PiEntityType entityType() {
         return PiEntityType.ACTION_PROFILE_GROUP;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
index b5df9d0..d850943 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
@@ -19,6 +19,7 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiActionProfileId;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -74,6 +75,11 @@
     }
 
     @Override
+    public PiActionProfileMemberHandle handle(DeviceId deviceId) {
+        return PiActionProfileMemberHandle.of(deviceId, this);
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) {
             return true;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
index 40cb960..7dda091 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
@@ -27,7 +27,7 @@
  * Global identifier of a PI action profile member, uniquely defined by a
  * device ID, action profile ID, and member ID.
  */
-public final class PiActionProfileMemberHandle extends PiHandle<PiActionProfileMember> {
+public final class PiActionProfileMemberHandle extends PiHandle {
 
     private final PiActionProfileId actionProfileId;
     private final PiActionProfileMemberId memberId;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCell.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCell.java
index 9508cbc..34ebd7b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCell.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCell.java
@@ -19,21 +19,23 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
 
 /**
  * Counter cell of a protocol-independent pipeline.
  */
 @Beta
-public final class PiCounterCell {
+public final class PiCounterCell implements PiEntity {
 
     private final PiCounterCellId cellId;
     private final PiCounterCellData counterData;
 
     /**
-     * Creates a new counter cell for the given cell identifier and counter cell data.
+     * Creates a new counter cell for the given cell identifier and counter cell
+     * data.
      *
-     * @param cellId  counter cell identifier
-     * @param piCounterCellData  counter cell data
+     * @param cellId            counter cell identifier
+     * @param piCounterCellData counter cell data
      */
     public PiCounterCell(PiCounterCellId cellId, PiCounterCellData piCounterCellData) {
         this.cellId = cellId;
@@ -41,11 +43,12 @@
     }
 
     /**
-     * Creates a new counter cell for the given cell identifier, number of packets and bytes.
+     * Creates a new counter cell for the given cell identifier, number of
+     * packets and bytes.
      *
      * @param cellId  counter cell identifier
-     * @param packets  number of packets
-     * @param bytes  number of bytes
+     * @param packets number of packets
+     * @param bytes   number of bytes
      */
     public PiCounterCell(PiCounterCellId cellId, long packets, long bytes) {
         this.cellId = cellId;
@@ -71,6 +74,16 @@
     }
 
     @Override
+    public PiEntityType piEntityType() {
+        return PiEntityType.COUNTER_CELL;
+    }
+
+    @Override
+    public PiCounterCellHandle handle(DeviceId deviceId) {
+        return PiCounterCellHandle.of(deviceId, this);
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) {
             return true;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellHandle.java
new file mode 100644
index 0000000..c06fa52
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellHandle.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Global identifier of a PI counter cell instantiated on a device, uniquely
+ * defined by a device ID and cell ID.
+ */
+public final class PiCounterCellHandle extends PiHandle {
+
+    private final PiCounterCellId cellId;
+
+    private PiCounterCellHandle(DeviceId deviceId, PiCounterCellId cellId) {
+        super(deviceId);
+        this.cellId = checkNotNull(cellId);
+    }
+
+    /**
+     * Creates a new handle for the given device ID and counter cell ID.
+     *
+     * @param deviceId device ID
+     * @param cellId counter cell ID
+     * @return new counter cell handle
+     */
+    public static PiCounterCellHandle of(DeviceId deviceId, PiCounterCellId cellId) {
+        return new PiCounterCellHandle(deviceId, cellId);
+    }
+
+    /**
+     * Creates a new handle for the given device ID and counter cell.
+     *
+     * @param deviceId device ID
+     * @param cell counter cell
+     * @return new counter cell handle
+     */
+    public static PiCounterCellHandle of(DeviceId deviceId, PiCounterCell cell) {
+        return new PiCounterCellHandle(deviceId, cell.cellId());
+    }
+
+    /**
+     * Returns the counter cell ID associated with this handle.
+     *
+     * @return counter cell ID
+     */
+    public PiCounterCellId cellId() {
+        return cellId;
+    }
+
+    @Override
+    public PiEntityType entityType() {
+        return PiEntityType.COUNTER_CELL;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(deviceId(), cellId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final PiCounterCellHandle other = (PiCounterCellHandle) obj;
+        return Objects.equal(this.deviceId(), other.deviceId())
+                && Objects.equal(this.cellId, other.cellId);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceId", deviceId())
+                .add("cellId", cellId)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java
index c3d5a01..531a6a2 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java
@@ -17,6 +17,7 @@
 package org.onosproject.net.pi.runtime;
 
 import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
 
 /**
  * Abstraction of an entity of a protocol-independent that can be read or write
@@ -31,4 +32,12 @@
      * @return entity type
      */
     PiEntityType piEntityType();
+
+    /**
+     * Returns a handle for this PI entity and the given device ID.
+     *
+     * @param deviceId device ID
+     * @return handle
+     */
+    PiHandle handle(DeviceId deviceId);
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
index 4c31830..0e99188 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
@@ -26,35 +26,56 @@
     /**
      * Table entry.
      */
-    TABLE_ENTRY,
+    TABLE_ENTRY("table entry"),
 
     /**
      * Action profile group.
      */
-    ACTION_PROFILE_GROUP,
+    ACTION_PROFILE_GROUP("action profile group"),
 
     /**
      * Action profile member.
      */
-    ACTION_PROFILE_MEMBER,
+    ACTION_PROFILE_MEMBER("action profile member"),
 
     /**
-     * Meter config.
+     * Meter cell config.
      */
-    METER_CELL_CONFIG,
+    METER_CELL_CONFIG("meter cell config"),
 
     /**
-     * Register entry.
+     * Register cell.
      */
-    REGISTER_CELL,
+    REGISTER_CELL("register cell"),
+
+    /**
+     * Counter cell.
+     */
+    COUNTER_CELL("counter cell"),
 
     /**
      * Packet Replication Engine (PRE) multicast group entry.
      */
-    PRE_MULTICAST_GROUP_ENTRY,
+    PRE_MULTICAST_GROUP_ENTRY("PRE multicast group entry"),
 
     /**
      * Packet Replication Engine (PRE) clone session entry.
      */
-    PRE_CLONE_SESSION_ENTRY
+    PRE_CLONE_SESSION_ENTRY("PRE clone session entry");
+
+    private final String humanReadableName;
+
+    PiEntityType(String humanReadableName) {
+        this.humanReadableName = humanReadableName;
+    }
+
+    /**
+     * Returns a human readable representation of this PI entity type (useful
+     * for logging).
+     *
+     * @return string
+     */
+    public String humanReadableName() {
+        return humanReadableName;
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java
index eb74288..c959096 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java
@@ -26,7 +26,7 @@
  * the whole network.
  */
 @Beta
-public abstract class PiHandle<E extends PiEntity> {
+public abstract class PiHandle {
 
     private final DeviceId deviceId;
 
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
index a978bfa..4ce849f 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
@@ -31,7 +31,7 @@
 @Beta
 public final class PiMatchKey {
 
-    public static final PiMatchKey EMPTY = builder().build();
+    public static final PiMatchKey EMPTY = new PiMatchKey(ImmutableMap.of());
 
     private final ImmutableMap<PiMatchFieldId, PiFieldMatch> fieldMatches;
 
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterBand.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterBand.java
index efa53ff..b1dbcf5 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterBand.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterBand.java
@@ -26,7 +26,7 @@
  * Represents a band used within a meter.
  */
 @Beta
-public class PiMeterBand {
+public final class PiMeterBand {
     private final long rate;
     private final long burst;
 
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellConfig.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellConfig.java
index 96ba124..f13e276 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellConfig.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellConfig.java
@@ -20,6 +20,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
+import org.onosproject.net.DeviceId;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -71,6 +72,11 @@
     }
 
     @Override
+    public PiMeterCellHandle handle(DeviceId deviceId) {
+        return PiMeterCellHandle.of(deviceId, this);
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) {
             return true;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellHandle.java
similarity index 72%
rename from core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterHandle.java
rename to core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellHandle.java
index 4baa6fa..382f60b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMeterCellHandle.java
@@ -24,15 +24,15 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Global identifier of a PI meter cell configuration applied to a device,
- * uniquely defined by a device ID and meter cell ID.
+ * Global identifier of a PI meter cell instantiated on a device, uniquely
+ * defined by a device ID and meter cell ID.
  */
 @Beta
-public final class PiMeterHandle extends PiHandle<PiMeterCellConfig> {
+public final class PiMeterCellHandle extends PiHandle {
 
     private final PiMeterCellId cellId;
 
-    private PiMeterHandle(DeviceId deviceId, PiMeterCellId meterCellId) {
+    private PiMeterCellHandle(DeviceId deviceId, PiMeterCellId meterCellId) {
         super(deviceId);
         this.cellId = meterCellId;
     }
@@ -44,9 +44,9 @@
      * @param meterCellId meter cell ID
      * @return PI meter handle
      */
-    public static PiMeterHandle of(DeviceId deviceId,
-                                   PiMeterCellId meterCellId) {
-        return new PiMeterHandle(deviceId, meterCellId);
+    public static PiMeterCellHandle of(DeviceId deviceId,
+                                       PiMeterCellId meterCellId) {
+        return new PiMeterCellHandle(deviceId, meterCellId);
     }
 
     /**
@@ -57,10 +57,19 @@
      * @param meterCellConfig meter config
      * @return PI meter handle
      */
-    public static PiMeterHandle of(DeviceId deviceId,
-                                   PiMeterCellConfig meterCellConfig) {
+    public static PiMeterCellHandle of(DeviceId deviceId,
+                                       PiMeterCellConfig meterCellConfig) {
         checkNotNull(meterCellConfig);
-        return new PiMeterHandle(deviceId, meterCellConfig.cellId());
+        return new PiMeterCellHandle(deviceId, meterCellConfig.cellId());
+    }
+
+    /**
+     * Returns the cell ID associated with this handle.
+     *
+     * @return cell ID
+     */
+    public PiMeterCellId cellId() {
+        return cellId;
     }
 
     @Override
@@ -81,7 +90,7 @@
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
-        PiMeterHandle that = (PiMeterHandle) o;
+        PiMeterCellHandle that = (PiMeterCellHandle) o;
         return Objects.equal(deviceId(), that.deviceId()) &&
                 Objects.equal(cellId, that.cellId);
     }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java
index 41b93f6..7a833aa 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java
@@ -20,6 +20,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.DeviceId;
 
 import java.util.Collection;
 import java.util.Set;
@@ -71,6 +72,11 @@
     }
 
     @Override
+    public PiMulticastGroupEntryHandle handle(DeviceId deviceId) {
+        return PiMulticastGroupEntryHandle.of(deviceId, this);
+    }
+
+    @Override
     public int hashCode() {
         return Objects.hashCode(groupId, replicas);
     }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java
index 65a3f28..b74ca8e 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java
@@ -29,11 +29,11 @@
  * ID.
  */
 @Beta
-public final class PiMulticastGroupEntryHandle extends PiHandle<PiMulticastGroupEntry> {
+public final class PiMulticastGroupEntryHandle extends PiHandle {
 
-    private final long groupId;
+    private final int groupId;
 
-    private PiMulticastGroupEntryHandle(DeviceId deviceId, long groupId) {
+    private PiMulticastGroupEntryHandle(DeviceId deviceId, int groupId) {
         super(deviceId);
         this.groupId = groupId;
     }
@@ -46,7 +46,7 @@
      * @return PI multicast group entry handle
      */
     public static PiMulticastGroupEntryHandle of(DeviceId deviceId,
-                                                 long groupId) {
+                                                 int groupId) {
         return new PiMulticastGroupEntryHandle(deviceId, groupId);
     }
 
@@ -64,6 +64,15 @@
         return new PiMulticastGroupEntryHandle(deviceId, entry.groupId());
     }
 
+    /**
+     * Returns the multicast group ID associated with this handle.
+     *
+     * @return group ID
+     */
+    public int groupId() {
+        return groupId;
+    }
+
     @Override
     public PiEntityType entityType() {
         return PiEntityType.PRE_MULTICAST_GROUP_ENTRY;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiControlMetadata.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketMetadata.java
similarity index 67%
rename from core/api/src/main/java/org/onosproject/net/pi/runtime/PiControlMetadata.java
rename to core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketMetadata.java
index 3caa71c..385f099 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiControlMetadata.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketMetadata.java
@@ -19,36 +19,40 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.Objects;
 import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Instance of a control metadata for a protocol-independent pipeline.
+ * Instance of a metadata field for a controller packet-in/out for a
+ * protocol-independent pipeline. Metadata are used to carry information other
+ * than the packet-in/out payload, such as the original ingress port of a
+ * packet-in, or the egress port of packet-out.
  */
 @Beta
-public final class PiControlMetadata {
+public final class PiPacketMetadata {
 
-    private final PiControlMetadataId id;
+    private final PiPacketMetadataId id;
     private final ImmutableByteSequence value;
 
     /**
-     * Creates a new control metadata instance for the given identifier and value.
+     * Creates a new packet metadata instance for the given identifier and
+     * value.
      *
-     * @param id    control metadata identifier
+     * @param id    packet metadata identifier
      * @param value value for this metadata
      */
-    private PiControlMetadata(PiControlMetadataId id, ImmutableByteSequence value) {
+    private PiPacketMetadata(PiPacketMetadataId id, ImmutableByteSequence value) {
         this.id = id;
         this.value = value;
     }
 
     /**
-     * Return the identifier of this control metadata.
+     * Return the identifier of this packet metadata.
      *
-     * @return control metadata identifier
+     * @return packet metadata identifier
      */
-    public PiControlMetadataId id() {
+    public PiPacketMetadataId id() {
         return id;
     }
 
@@ -69,7 +73,7 @@
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
-        PiControlMetadata piPacket = (PiControlMetadata) o;
+        PiPacketMetadata piPacket = (PiPacketMetadata) o;
         return Objects.equal(id, piPacket.id()) &&
                 Objects.equal(value, piPacket.value());
     }
@@ -85,7 +89,7 @@
     }
 
     /**
-     * Returns a control metadata builder.
+     * Returns a packet metadata builder.
      *
      * @return a new builder
      */
@@ -94,11 +98,11 @@
     }
 
     /**
-     * Builder of protocol-independent control metadatas.
+     * Builder of protocol-independent packet metadatas.
      */
     public static final class Builder {
 
-        private PiControlMetadataId id;
+        private PiPacketMetadataId id;
         private ImmutableByteSequence value;
 
         private Builder() {
@@ -106,12 +110,12 @@
         }
 
         /**
-         * Sets the identifier of this control metadata.
+         * Sets the identifier of this packet metadata.
          *
-         * @param id control metadata identifier
+         * @param id packet metadata identifier
          * @return this
          */
-        public Builder withId(PiControlMetadataId id) {
+        public Builder withId(PiPacketMetadataId id) {
             this.id = id;
             return this;
         }
@@ -128,14 +132,14 @@
         }
 
         /**
-         * Returns a new control metadata instance.
+         * Returns a new packet metadata instance.
          *
-         * @return control metadata
+         * @return packet metadata
          */
-        public PiControlMetadata build() {
+        public PiPacketMetadata build() {
             checkNotNull(id);
             checkNotNull(value);
-            return new PiControlMetadata(id, value);
+            return new PiPacketMetadata(id, value);
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketOperation.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketOperation.java
index 74c68d6..5811b97 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketOperation.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPacketOperation.java
@@ -21,8 +21,7 @@
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableSet;
 import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 import org.onosproject.net.pi.model.PiPacketOperationType;
 
 import java.util.Collection;
@@ -33,43 +32,33 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Instance of a packet I/O operation, and its control metadatas, for a protocol-independent pipeline.
+ * Instance of a packet I/O operation that includes the packet body (frame) and
+ * its metadata, for a protocol-independent pipeline.
  */
 @Beta
 public final class PiPacketOperation {
 
-    private final DeviceId deviceId;
-    private final ImmutableByteSequence data;
-    private final Set<PiControlMetadata> packetMetadatas;
+    private final ImmutableByteSequence frame;
+    private final Set<PiPacketMetadata> packetMetadatas;
     private final PiPacketOperationType type;
 
     /**
-     * Creates a new packet I/O operation for the given device ID, data, control metadatas and operation type.
+     * Creates a new packet I/O operation for the given frame, packet metadata
+     * and operation type.
      *
-     * @param deviceId        device ID
-     * @param data            the packet raw data
-     * @param packetMetadatas collection of control metadata
-     * @param type            type of this packet operation
+     * @param frame     the packet raw data
+     * @param metadatas collection of packet metadata
+     * @param type      type of this packet operation
      */
-    private PiPacketOperation(DeviceId deviceId, ImmutableByteSequence data,
-                              Collection<PiControlMetadata> packetMetadatas,
+    private PiPacketOperation(ImmutableByteSequence frame,
+                              Collection<PiPacketMetadata> metadatas,
                               PiPacketOperationType type) {
-        this.deviceId = deviceId;
-        this.data = data;
-        this.packetMetadatas = ImmutableSet.copyOf(packetMetadatas);
+        this.frame = frame;
+        this.packetMetadatas = ImmutableSet.copyOf(metadatas);
         this.type = type;
     }
 
     /**
-     * Returns the device ID of this packet operation.
-     *
-     * @return device ID
-     */
-    public DeviceId deviceId() {
-        return deviceId;
-    }
-
-    /**
      * Return the type of this packet.
      *
      * @return packet type
@@ -84,15 +73,16 @@
      * @return packet data
      */
     public ImmutableByteSequence data() {
-        return data;
+        return frame;
     }
 
     /**
-     * Returns all metadatas of this packet. Returns an empty collection if the packet doesn't have any metadata.
+     * Returns all metadatas of this packet. Returns an empty collection if the
+     * packet doesn't have any metadata.
      *
      * @return collection of metadatas
      */
-    public Collection<PiControlMetadata> metadatas() {
+    public Collection<PiPacketMetadata> metadatas() {
         return packetMetadatas;
     }
 
@@ -106,22 +96,21 @@
         }
         PiPacketOperation that = (PiPacketOperation) o;
         return Objects.equal(packetMetadatas, that.packetMetadatas) &&
-                Objects.equal(deviceId, that.deviceId) &&
-                Objects.equal(data, that.data()) &&
+                Objects.equal(frame, that.data()) &&
                 Objects.equal(type, that.type());
     }
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(deviceId, data, packetMetadatas, type);
+        return Objects.hashCode(frame, packetMetadatas, type);
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
-                .add("deviceId", deviceId)
-                .addValue(type.toString())
-                .addValue(packetMetadatas)
+                .add("type", type)
+                .add("metadata", packetMetadatas)
+                .add("frame", frame)
                 .toString();
     }
 
@@ -139,8 +128,7 @@
      */
     public static final class Builder {
 
-        private DeviceId deviceId;
-        private Map<PiControlMetadataId, PiControlMetadata> packetMetadatas = new HashMap<>();
+        private Map<PiPacketMetadataId, PiPacketMetadata> packetMetadatas = new HashMap<>();
         private PiPacketOperationType type;
         private ImmutableByteSequence data;
 
@@ -149,18 +137,6 @@
         }
 
         /**
-         * Sets the device ID.
-         *
-         * @param deviceId device ID
-         * @return this
-         */
-        public Builder forDevice(DeviceId deviceId) {
-            checkNotNull(deviceId);
-            this.deviceId = deviceId;
-            return this;
-        }
-
-        /**
          * Sets the raw packet data.
          *
          * @param data the packet raw data
@@ -173,13 +149,14 @@
         }
 
         /**
-         * Adds a control metadata. Only one metadata is allowed for a given metadata id. If a metadata with same id
-         * already exists it will be replaced by the given one.
+         * Adds a packet metadata. Only one metadata is allowed for a given
+         * metadata id. If a metadata with same id already exists it will be
+         * replaced by the given one.
          *
          * @param metadata packet metadata
          * @return this
          */
-        public Builder withMetadata(PiControlMetadata metadata) {
+        public Builder withMetadata(PiPacketMetadata metadata) {
             checkNotNull(metadata);
             packetMetadatas.put(metadata.id(), metadata);
 
@@ -192,7 +169,7 @@
          * @param metadatas collection of metadata
          * @return this
          */
-        public Builder withMetadatas(Collection<PiControlMetadata> metadatas) {
+        public Builder withMetadatas(Collection<PiPacketMetadata> metadatas) {
             checkNotNull(metadatas);
             metadatas.forEach(this::withMetadata);
             return this;
@@ -215,11 +192,10 @@
          * @return packet operation
          */
         public PiPacketOperation build() {
-            checkNotNull(deviceId);
             checkNotNull(data);
             checkNotNull(packetMetadatas);
             checkNotNull(type);
-            return new PiPacketOperation(deviceId, data, packetMetadatas.values(), type);
+            return new PiPacketOperation(data, packetMetadatas.values(), type);
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java
index 4f65e4d..3a03feb 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.net.pi.runtime;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Objects;
 import org.onosproject.net.PortNumber;
 
@@ -29,7 +30,8 @@
  * Each replica is uniquely identified inside a given multicast group or clone
  * session by the pair (egress port, instance ID).
  */
-public class PiPreReplica {
+@Beta
+public final class PiPreReplica {
 
     private final PortNumber egressPort;
     private final int instanceId;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiRegisterCell.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiRegisterCell.java
index fd354ec..c50771b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiRegisterCell.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiRegisterCell.java
@@ -19,6 +19,7 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiData;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -61,6 +62,12 @@
     }
 
     @Override
+    public PiHandle handle(DeviceId deviceId) {
+        // TODO: implement support for register cell handles
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
     public boolean equals(Object obj) {
         if (this == obj) {
             return true;
@@ -137,4 +144,4 @@
             return new PiRegisterCell(cellId, piData);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index b25ada5..ee3c3c3 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -19,9 +19,11 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiTableId;
 
 import java.util.Optional;
+import java.util.OptionalInt;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -112,8 +114,8 @@
      *
      * @return optional priority
      */
-    public Optional<Integer> priority() {
-        return priority == NO_PRIORITY ? Optional.empty() : Optional.of(priority);
+    public OptionalInt priority() {
+        return priority == NO_PRIORITY ? OptionalInt.empty() : OptionalInt.of(priority);
     }
 
     /**
@@ -203,6 +205,11 @@
         return PiEntityType.TABLE_ENTRY;
     }
 
+    @Override
+    public PiTableEntryHandle handle(DeviceId deviceId) {
+        return PiTableEntryHandle.of(deviceId, this);
+    }
+
     public static final class Builder {
 
         private PiTableId tableId;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java
index 2b210a1..b4edb3c 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java
@@ -22,6 +22,8 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.model.PiTableId;
 
+import java.util.OptionalInt;
+
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
@@ -29,30 +31,20 @@
  * by a device ID, table ID and match key.
  */
 @Beta
-public final class PiTableEntryHandle extends PiHandle<PiTableEntry> {
+public final class PiTableEntryHandle extends PiHandle {
+
+    private static final int NO_PRIORITY = -1;
 
     private final PiTableId tableId;
     private final PiMatchKey matchKey;
+    private final int priority;
 
-    private PiTableEntryHandle(DeviceId deviceId, PiTableId tableId, PiMatchKey matchKey) {
+    private PiTableEntryHandle(DeviceId deviceId, PiTableId tableId, PiMatchKey matchKey,
+                               Integer priority) {
         super(deviceId);
         this.tableId = tableId;
         this.matchKey = matchKey;
-    }
-
-    /**
-     * Creates a new handle for the given device ID, PI table ID, and match
-     * key.
-     *
-     * @param deviceId device ID
-     * @param tableId  table ID
-     * @param matchKey match key
-     * @return PI table entry handle
-     */
-    public static PiTableEntryHandle of(DeviceId deviceId, PiTableId tableId, PiMatchKey matchKey) {
-        checkNotNull(tableId);
-        checkNotNull(matchKey);
-        return new PiTableEntryHandle(deviceId, tableId, matchKey);
+        this.priority = priority;
     }
 
     /**
@@ -64,7 +56,37 @@
      */
     public static PiTableEntryHandle of(DeviceId deviceId, PiTableEntry entry) {
         checkNotNull(entry);
-        return PiTableEntryHandle.of(deviceId, entry.table(), entry.matchKey());
+        return new PiTableEntryHandle(
+                deviceId, entry.table(), entry.matchKey(),
+                entry.priority().orElse(NO_PRIORITY));
+    }
+
+    /**
+     * Returns the table ID associated with this handle.
+     *
+     * @return table ID
+     */
+    public PiTableId tableId() {
+        return tableId;
+    }
+
+    /**
+     * Returns the match key associated with this handle.
+     *
+     * @return match key
+     */
+    public PiMatchKey matchKey() {
+        return matchKey;
+    }
+
+    /**
+     * Returns the optional priority associated with this handle.
+     *
+     * @return optional priority
+     */
+    public OptionalInt priority() {
+        return priority == NO_PRIORITY
+                ? OptionalInt.empty() : OptionalInt.of(priority);
     }
 
     @Override
@@ -74,7 +96,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(deviceId(), tableId, matchKey);
+        return Objects.hashCode(deviceId(), tableId, matchKey, priority().orElse(NO_PRIORITY));
     }
 
     @Override
@@ -88,7 +110,8 @@
         final PiTableEntryHandle other = (PiTableEntryHandle) obj;
         return Objects.equal(this.deviceId(), other.deviceId())
                 && Objects.equal(this.tableId, other.tableId)
-                && Objects.equal(this.matchKey, other.matchKey);
+                && Objects.equal(this.matchKey, other.matchKey)
+                && Objects.equal(this.priority(), other.priority());
     }
 
     @Override
@@ -97,6 +120,7 @@
                 .add("deviceId", deviceId())
                 .add("tableId", tableId)
                 .add("matchKey", matchKey)
+                .add("priority", priority == NO_PRIORITY ? "N/A" : priority)
                 .toString();
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiPipeconfService.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiPipeconfService.java
index e9539a5..9943352 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiPipeconfService.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiPipeconfService.java
@@ -71,6 +71,16 @@
     Optional<PiPipeconf> getPipeconf(PiPipeconfId id);
 
     /**
+     * Returns the pipeconf instance associated with the given device, if
+     * present. If not present, it means no pipeconf has been associated with
+     * that device so far.
+     *
+     * @param deviceId a device identifier
+     * @return an optional pipeconf
+     */
+    Optional<PiPipeconf> getPipeconf(DeviceId deviceId);
+
+    /**
      * Signals that the given pipeconf is associated to the given infrastructure
      * device. As a result of this method, the pipeconf for the given device can
      * be later retrieved using {@link #ofDevice(DeviceId)}
@@ -107,7 +117,9 @@
      *
      * @param deviceId device identifier
      * @return an optional pipeconf identifier
+     * @deprecated in ONOS 2.1 use {@link #getPipeconf(DeviceId)} instead
      */
+    @Deprecated
     Optional<PiPipeconfId> ofDevice(DeviceId deviceId);
 
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java
index 4ca094b..3c3d9f6 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java
@@ -32,7 +32,7 @@
 
     private final T original;
     private final E translated;
-    private final PiHandle<E> handle;
+    private final PiHandle handle;
 
     /**
      * Creates a new translated entity.
@@ -41,7 +41,7 @@
      * @param translated PI entity
      * @param handle PI entity handle
      */
-    public PiTranslatedEntity(T original, E translated, PiHandle<E> handle) {
+    public PiTranslatedEntity(T original, E translated, PiHandle handle) {
         this.original = checkNotNull(original);
         this.translated = checkNotNull(translated);
         this.handle = checkNotNull(handle);
@@ -79,7 +79,7 @@
      *
      * @return PI entity handle
      */
-    public final PiHandle<E> handle() {
+    public final PiHandle handle() {
         return handle;
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java
index 6274deb..a1f3a60 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java
@@ -39,7 +39,7 @@
      * @param handle PI entity handle
      * @param entity PI translated entity
      */
-    void addOrUpdate(PiHandle<E> handle, PiTranslatedEntity<T, E> entity);
+    void addOrUpdate(PiHandle handle, PiTranslatedEntity<T, E> entity);
 
     /**
      * Returns a PI translated entity for the given handle. Returns null if this
@@ -49,12 +49,12 @@
      * @param handle PI entity handle
      * @return PI translated entity
      */
-    PiTranslatedEntity<T, E> get(PiHandle<E> handle);
+    PiTranslatedEntity<T, E> get(PiHandle handle);
 
     /**
      * Removes a previously added mapping for the given PI entity handle.
      *
      * @param handle PI entity handle
      */
-    void remove(PiHandle<E> handle);
+    void remove(PiHandle handle);
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java
index 202636a..499fdc5 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java
@@ -52,7 +52,7 @@
      * @param handle PI entity handle
      * @param entity PI translated entity
      */
-    void learn(PiHandle<E> handle, PiTranslatedEntity<T, E> entity);
+    void learn(PiHandle handle, PiTranslatedEntity<T, E> entity);
 
     /**
      * Returns a PI translated entity that was previously associated with the
@@ -64,12 +64,12 @@
      * @param handle PI entity handle
      * @return optional PI translated entity
      */
-    Optional<PiTranslatedEntity<T, E>> lookup(PiHandle<E> handle);
+    Optional<PiTranslatedEntity<T, E>> lookup(PiHandle handle);
 
     /**
      * Removes any mapping for the given PI entity handle.
      *
      * @param handle PI entity handle.
      */
-    void forget(PiHandle<E> handle);
+    void forget(PiHandle handle);
 }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/PiPipeconfServiceAdapter.java b/core/api/src/test/java/org/onosproject/net/pi/PiPipeconfServiceAdapter.java
index 8eebf5f..66f9315 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/PiPipeconfServiceAdapter.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/PiPipeconfServiceAdapter.java
@@ -49,6 +49,11 @@
     }
 
     @Override
+    public Optional<PiPipeconf> getPipeconf(DeviceId deviceId) {
+        return Optional.empty();
+    }
+
+    @Override
     public void bindToDevice(PiPipeconfId pipeconfId, DeviceId deviceId) {
 
     }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataTest.java
deleted file mode 100644
index ab80fd6..0000000
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.net.pi.runtime;
-
-import com.google.common.testing.EqualsTester;
-import org.junit.Test;
-import org.onosproject.net.pi.model.PiControlMetadataId;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onosproject.net.pi.runtime.PiConstantsTest.EGRESS_PORT;
-
-/**
- * Unit tests for PiControlMetadata class.
- */
-public class PiControlMetadataTest {
-
-    final PiControlMetadataId piControlMetadataId = PiControlMetadataId.of(EGRESS_PORT);
-
-    final PiControlMetadata piControlMetadata1 = PiControlMetadata.builder()
-            .withId(piControlMetadataId)
-            .withValue(copyFrom(0x10))
-            .build();
-    final PiControlMetadata sameAsPiControlMetadata1 = PiControlMetadata.builder()
-            .withId(piControlMetadataId)
-            .withValue(copyFrom(0x10))
-            .build();
-    final PiControlMetadata piControlMetadata2 = PiControlMetadata.builder()
-            .withId(piControlMetadataId)
-            .withValue(copyFrom(0x20))
-            .build();
-
-    /**
-     * Checks that the PiControlMetadata class is immutable.
-     */
-    @Test
-    public void testImmutability() {
-
-        assertThatClassIsImmutable(PiControlMetadata.class);
-    }
-
-    /**
-     * Checks the operation of equals(), hashCode() and toString() methods.
-     */
-    @Test
-    public void testEquals() {
-
-        new EqualsTester()
-                .addEqualityGroup(piControlMetadata1, sameAsPiControlMetadata1)
-                .addEqualityGroup(piControlMetadata2)
-                .testEquals();
-    }
-
-    /**
-     * Checks the methods of PiControlMetadata.
-     */
-    @Test
-    public void testMethods() {
-
-        assertThat(piControlMetadata1, is(notNullValue()));
-        assertThat(piControlMetadata1.id(), is(PiControlMetadataId.of(EGRESS_PORT)));
-        assertThat(piControlMetadata1.value(), is(copyFrom(0x10)));
-    }
-}
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataIdTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataIdTest.java
similarity index 62%
rename from core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataIdTest.java
rename to core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataIdTest.java
index d65ef2e..310b836 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiControlMetadataIdTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataIdTest.java
@@ -18,7 +18,7 @@
 
 import com.google.common.testing.EqualsTester;
 import org.junit.Test;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -28,21 +28,21 @@
 import static org.onosproject.net.pi.runtime.PiConstantsTest.INGRESS_PORT;
 
 /**
- * Unit tests for PiControlMetadataId class.
+ * Unit tests for PiPacketMetadataId class.
  */
-public class PiControlMetadataIdTest {
+public class PiPacketMetadataIdTest {
 
-    final PiControlMetadataId piControlMetadataId1 = PiControlMetadataId.of(EGRESS_PORT);
-    final PiControlMetadataId sameAsPiControlMetadataId1 = PiControlMetadataId.of(EGRESS_PORT);
-    final PiControlMetadataId piControlMetadataId2 = PiControlMetadataId.of(INGRESS_PORT);
+    final PiPacketMetadataId piPacketMetadataId1 = PiPacketMetadataId.of(EGRESS_PORT);
+    final PiPacketMetadataId sameAsPiPacketMetadataId1 = PiPacketMetadataId.of(EGRESS_PORT);
+    final PiPacketMetadataId piPacketMetadataId2 = PiPacketMetadataId.of(INGRESS_PORT);
 
     /**
-     * Checks that the PiControlMetadataId class is immutable.
+     * Checks that the PiPacketMetadataId class is immutable.
      */
     @Test
     public void testImmutability() {
 
-        assertThatClassIsImmutable(PiControlMetadataId.class);
+        assertThatClassIsImmutable(PiPacketMetadataId.class);
     }
 
     /**
@@ -52,17 +52,17 @@
     public void testEquals() {
 
         new EqualsTester()
-                .addEqualityGroup(piControlMetadataId1, sameAsPiControlMetadataId1)
-                .addEqualityGroup(piControlMetadataId2)
+                .addEqualityGroup(piPacketMetadataId1, sameAsPiPacketMetadataId1)
+                .addEqualityGroup(piPacketMetadataId2)
                 .testEquals();
     }
 
     /**
-     * Checks the methods of PiControlMetadataId.
+     * Checks the methods of PiPacketMetadataId.
      */
     @Test
     public void testMethods() {
-        assertThat(piControlMetadataId1, is(notNullValue()));
-        assertThat(piControlMetadataId1.id(), is(EGRESS_PORT));
+        assertThat(piPacketMetadataId1, is(notNullValue()));
+        assertThat(piPacketMetadataId1.id(), is(EGRESS_PORT));
     }
 }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataTest.java
new file mode 100644
index 0000000..58cd252
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketMetadataTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.EGRESS_PORT;
+
+/**
+ * Unit tests for PiPacketMetadata class.
+ */
+public class PiPacketMetadataTest {
+
+    final PiPacketMetadataId piPacketMetadataId = PiPacketMetadataId.of(EGRESS_PORT);
+
+    final PiPacketMetadata piPacketMetadata1 = PiPacketMetadata.builder()
+            .withId(piPacketMetadataId)
+            .withValue(copyFrom(0x10))
+            .build();
+    final PiPacketMetadata sameAsPiPacketMetadata1 = PiPacketMetadata.builder()
+            .withId(piPacketMetadataId)
+            .withValue(copyFrom(0x10))
+            .build();
+    final PiPacketMetadata piPacketMetadata2 = PiPacketMetadata.builder()
+            .withId(piPacketMetadataId)
+            .withValue(copyFrom(0x20))
+            .build();
+
+    /**
+     * Checks that the PiPacketMetadata class is immutable.
+     */
+    @Test
+    public void testImmutability() {
+
+        assertThatClassIsImmutable(PiPacketMetadata.class);
+    }
+
+    /**
+     * Checks the operation of equals(), hashCode() and toString() methods.
+     */
+    @Test
+    public void testEquals() {
+
+        new EqualsTester()
+                .addEqualityGroup(piPacketMetadata1, sameAsPiPacketMetadata1)
+                .addEqualityGroup(piPacketMetadata2)
+                .testEquals();
+    }
+
+    /**
+     * Checks the methods of PiPacketMetadata.
+     */
+    @Test
+    public void testMethods() {
+
+        assertThat(piPacketMetadata1, is(notNullValue()));
+        assertThat(piPacketMetadata1.id(), is(PiPacketMetadataId.of(EGRESS_PORT)));
+        assertThat(piPacketMetadata1.value(), is(copyFrom(0x10)));
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketOperationTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketOperationTest.java
index d675b51..e924914 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketOperationTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPacketOperationTest.java
@@ -21,8 +21,7 @@
 import org.apache.commons.collections.CollectionUtils;
 import org.junit.Test;
 import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -37,34 +36,29 @@
  */
 public class PiPacketOperationTest {
 
-    private final DeviceId deviceId = DeviceId.deviceId("dummy");
-
     private final PiPacketOperation piPacketOperation1 = PiPacketOperation.builder()
-            .forDevice(deviceId)
             .withData(ImmutableByteSequence.ofOnes(512))
             .withType(PACKET_OUT)
-            .withMetadata(PiControlMetadata.builder()
-                                  .withId(PiControlMetadataId.of(EGRESS_PORT))
+            .withMetadata(PiPacketMetadata.builder()
+                                  .withId(PiPacketMetadataId.of(EGRESS_PORT))
                                   .withValue(copyFrom((short) 255))
                                   .build())
             .build();
 
     private final PiPacketOperation sameAsPiPacketOperation1 = PiPacketOperation.builder()
-            .forDevice(deviceId)
             .withData(ImmutableByteSequence.ofOnes(512))
             .withType(PACKET_OUT)
-            .withMetadata(PiControlMetadata.builder()
-                                  .withId(PiControlMetadataId.of(EGRESS_PORT))
+            .withMetadata(PiPacketMetadata.builder()
+                                  .withId(PiPacketMetadataId.of(EGRESS_PORT))
                                   .withValue(copyFrom((short) 255))
                                   .build())
             .build();
 
     private final PiPacketOperation piPacketOperation2 = PiPacketOperation.builder()
-            .forDevice(deviceId)
             .withData(ImmutableByteSequence.ofOnes(512))
             .withType(PACKET_OUT)
-            .withMetadata(PiControlMetadata.builder()
-                                  .withId(PiControlMetadataId.of(EGRESS_PORT))
+            .withMetadata(PiPacketMetadata.builder()
+                                  .withId(PiPacketMetadataId.of(EGRESS_PORT))
                                   .withValue(copyFrom((short) 200))
                                   .build())
             .build();
@@ -97,23 +91,21 @@
     public void testMethods() {
 
         final PiPacketOperation piPacketOperation = PiPacketOperation.builder()
-                .forDevice(deviceId)
                 .withData(ImmutableByteSequence.ofOnes(512))
                 .withType(PACKET_OUT)
-                .withMetadata(PiControlMetadata.builder()
-                                      .withId(PiControlMetadataId.of(EGRESS_PORT))
+                .withMetadata(PiPacketMetadata.builder()
+                                      .withId(PiPacketMetadataId.of(EGRESS_PORT))
                                       .withValue(copyFrom((short) 10))
                                       .build())
                 .build();
 
         assertThat(piPacketOperation, is(notNullValue()));
-        assertThat(piPacketOperation.deviceId(), is(deviceId));
         assertThat(piPacketOperation.type(), is(PACKET_OUT));
         assertThat(piPacketOperation.data(), is(ImmutableByteSequence.ofOnes(512)));
         assertThat("Incorrect metadatas value",
                    CollectionUtils.isEqualCollection(piPacketOperation.metadatas(),
-                                                     ImmutableList.of(PiControlMetadata.builder()
-                                                                              .withId(PiControlMetadataId
+                                                     ImmutableList.of(PiPacketMetadata.builder()
+                                                                              .withId(PiPacketMetadataId
                                                                                               .of(EGRESS_PORT))
                                                                               .withValue(copyFrom((short) 10))
                                                                               .build())));
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
index 087eb77..ee2fd78 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
@@ -110,7 +110,7 @@
         assertThat(piTableEntry.cookie(), is(cookie));
         assertThat("Priority must be set", piTableEntry.priority().isPresent());
         assertThat("Timeout must be set", piTableEntry.timeout().isPresent());
-        assertThat(piTableEntry.priority().get(), is(priority));
+        assertThat(piTableEntry.priority().getAsInt(), is(priority));
         assertThat(piTableEntry.timeout().get(), is(timeout));
         assertThat("Incorrect match param value",
                    CollectionUtils.isEqualCollection(piTableEntry.matchKey().fieldMatches(), fieldMatches.values()));
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java
index 5d7178c..dc2cb01 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java
@@ -42,17 +42,17 @@
     }
 
     @Override
-    public void learn(PiHandle<E> handle, PiTranslatedEntity<T, E> entity) {
+    public void learn(PiHandle handle, PiTranslatedEntity<T, E> entity) {
         store.addOrUpdate(handle, entity);
     }
 
     @Override
-    public Optional<PiTranslatedEntity<T, E>> lookup(PiHandle<E> handle) {
+    public Optional<PiTranslatedEntity<T, E>> lookup(PiHandle handle) {
         return Optional.ofNullable(store.get(handle));
     }
 
     @Override
-    public void forget(PiHandle<E> handle) {
+    public void forget(PiHandle handle) {
         store.remove(handle);
     }
 }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
index fa10d33..27f6e4d 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
@@ -169,6 +169,16 @@
     }
 
     @Override
+    public Optional<PiPipeconf> getPipeconf(DeviceId deviceId) {
+        if (pipeconfMappingStore.getPipeconfId(deviceId) == null) {
+            return Optional.empty();
+        } else {
+            return Optional.ofNullable(pipeconfs.get(
+                    pipeconfMappingStore.getPipeconfId(deviceId)));
+        }
+    }
+
+    @Override
     public void bindToDevice(PiPipeconfId pipeconfId, DeviceId deviceId) {
         PiPipeconfId existingPipeconfId = pipeconfMappingStore.getPipeconfId(deviceId);
         if (existingPipeconfId != null && !existingPipeconfId.equals(pipeconfId)) {
diff --git a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java
index 36d87b5..21a39bf 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java
@@ -56,11 +56,11 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
     protected StorageService storageService;
 
-    private EventuallyConsistentMap<PiHandle<E>, PiTranslatedEntity<T, E>>
+    private EventuallyConsistentMap<PiHandle, PiTranslatedEntity<T, E>>
             translatedEntities;
 
     private final EventuallyConsistentMapListener
-            <PiHandle<E>, PiTranslatedEntity<T, E>> entityMapListener =
+            <PiHandle, PiTranslatedEntity<T, E>> entityMapListener =
             new InternalEntityMapListener();
 
     /**
@@ -75,7 +75,7 @@
     public void activate() {
         final String fullMapName = format(MAP_NAME_TEMPLATE, mapSimpleName());
         translatedEntities = storageService
-                .<PiHandle<E>, PiTranslatedEntity<T, E>>eventuallyConsistentMapBuilder()
+                .<PiHandle, PiTranslatedEntity<T, E>>eventuallyConsistentMapBuilder()
                 .withName(fullMapName)
                 .withSerializer(KryoNamespaces.API)
                 .withTimestampProvider((k, v) -> new WallClockTimestamp())
@@ -92,7 +92,7 @@
     }
 
     @Override
-    public void addOrUpdate(PiHandle<E> handle, PiTranslatedEntity<T, E> entity) {
+    public void addOrUpdate(PiHandle handle, PiTranslatedEntity<T, E> entity) {
         checkNotNull(handle);
         checkNotNull(entity);
         checkArgument(handle.entityType().equals(entity.entityType()),
@@ -101,13 +101,13 @@
     }
 
     @Override
-    public void remove(PiHandle<E> handle) {
+    public void remove(PiHandle handle) {
         checkNotNull(handle);
         translatedEntities.remove(handle);
     }
 
     @Override
-    public PiTranslatedEntity<T, E> get(PiHandle<E> handle) {
+    public PiTranslatedEntity<T, E> get(PiHandle handle) {
         checkNotNull(handle);
         return translatedEntities.get(handle);
     }
@@ -118,10 +118,10 @@
 
     private class InternalEntityMapListener
             implements EventuallyConsistentMapListener
-                               <PiHandle<E>, PiTranslatedEntity<T, E>> {
+                               <PiHandle, PiTranslatedEntity<T, E>> {
 
         @Override
-        public void event(EventuallyConsistentMapEvent<PiHandle<E>,
+        public void event(EventuallyConsistentMapEvent<PiHandle,
                 PiTranslatedEntity<T, E>> event) {
             final PiTranslationEvent.Type type;
             switch (event.type()) {
diff --git a/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java
index acfce12..fea7392 100644
--- a/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java
+++ b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java
@@ -43,9 +43,8 @@
     private static final PiTranslatable PI_TRANSLATABLE =
             new PiTranslatable() {
             };
-    private static final PiEntity PI_ENTITY = () -> PiEntityType.TABLE_ENTRY;
-    private static final PiHandle<PiEntity> PI_HANDLE =
-            new PiHandle<PiEntity>(DeviceId.NONE) {
+    private static final PiHandle PI_HANDLE =
+            new PiHandle(DeviceId.NONE) {
                 @Override
                 public PiEntityType entityType() {
                     return PI_ENTITY.piEntityType();
@@ -66,6 +65,17 @@
                     return String.valueOf(HANDLE_HASH);
                 }
             };
+    private static final PiEntity PI_ENTITY = new PiEntity() {
+        @Override
+        public PiEntityType piEntityType() {
+            return PiEntityType.TABLE_ENTRY;
+        }
+
+        @Override
+        public PiHandle handle(DeviceId deviceId) {
+            return PI_HANDLE;
+        }
+    };
     private static final PiTranslatedEntity<PiTranslatable, PiEntity> TRANSLATED_ENTITY =
             new PiTranslatedEntity<>(PI_TRANSLATABLE, PI_ENTITY, PI_HANDLE);
 
diff --git a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
index 03bf350..5437a4c 100644
--- a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
@@ -213,7 +213,7 @@
 import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionParamId;
 import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 import org.onosproject.net.pi.model.PiCounterId;
 import org.onosproject.net.pi.model.PiCounterType;
 import org.onosproject.net.pi.model.PiMatchFieldId;
@@ -232,7 +232,7 @@
 import org.onosproject.net.pi.runtime.PiActionProfileMember;
 import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
 import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import org.onosproject.net.pi.runtime.PiControlMetadata;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
 import org.onosproject.net.pi.runtime.PiCounterCell;
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
@@ -676,7 +676,7 @@
                     PiActionId.class,
                     PiActionParamId.class,
                     PiActionProfileId.class,
-                    PiControlMetadataId.class,
+                    PiPacketMetadataId.class,
                     PiCounterId.class,
                     PiCounterType.class,
                     PiMatchFieldId.class,
@@ -697,7 +697,7 @@
                     PiActionProfileMemberHandle.class,
                     PiActionProfileMemberId.class,
                     PiActionParam.class,
-                    PiControlMetadata.class,
+                    PiPacketMetadata.class,
                     PiCounterCell.class,
                     PiCounterCellData.class,
                     PiCounterCellId.class,
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
index 41c2b68..8a9a403 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimeHandlerBehaviour.java
@@ -65,7 +65,7 @@
 
     /**
      * Initializes this behaviour attributes. Returns true if the operation was
-     * successful, false otherwise. This method assumes that the P4runtime
+     * successful, false otherwise. This method assumes that the P4Runtime
      * controller already has a client for this device and that the device has
      * been created in the core.
      *
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimePipelineProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimePipelineProgrammable.java
index c7d76c0..664bd59 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimePipelineProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/AbstractP4RuntimePipelineProgrammable.java
@@ -101,7 +101,7 @@
             return false;
         }
 
-        return client.isPipelineConfigSet(pipeconf, deviceDataBuffer);
+        return client.isPipelineConfigSetSync(pipeconf, deviceDataBuffer);
     }
 
     @Override
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
index d614d6e..fca05cb 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
@@ -16,14 +16,12 @@
 
 package org.onosproject.drivers.p4runtime;
 
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.Striped;
 import org.onlab.util.SharedExecutors;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileGroupMirror;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileMemberMirror;
+import org.onosproject.drivers.p4runtime.mirror.P4RuntimeMirror;
 import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.group.DefaultGroup;
@@ -34,17 +32,18 @@
 import org.onosproject.net.group.GroupOperations;
 import org.onosproject.net.group.GroupProgrammable;
 import org.onosproject.net.group.GroupStore;
-import org.onosproject.net.pi.model.PiActionProfileId;
 import org.onosproject.net.pi.model.PiActionProfileModel;
 import org.onosproject.net.pi.runtime.PiActionProfileGroup;
 import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
 import org.onosproject.net.pi.runtime.PiActionProfileMember;
 import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
 import org.onosproject.net.pi.service.PiGroupTranslator;
 import org.onosproject.net.pi.service.PiTranslatedEntity;
 import org.onosproject.net.pi.service.PiTranslationException;
-import org.onosproject.p4runtime.api.P4RuntimeClient;
+import org.onosproject.p4runtime.api.P4RuntimeReadClient;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -53,14 +52,10 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.locks.Lock;
 import java.util.stream.Collectors;
 
-import static java.util.Collections.singletonList;
 import static java.util.stream.Collectors.toMap;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY;
+import static java.util.stream.Collectors.toSet;
 
 /**
  * Implementation of GroupProgrammable to handle action profile groups in
@@ -80,9 +75,6 @@
     private P4RuntimeActionProfileMemberMirror memberMirror;
     private PiGroupTranslator groupTranslator;
 
-    // Needed to synchronize operations over the same group.
-    private static final Striped<Lock> STRIPED_LOCKS = Striped.lock(30);
-
     @Override
     protected boolean setupBehaviour() {
         if (!super.setupBehaviour()) {
@@ -134,25 +126,33 @@
         }
 
         // Dump groups and members from device for all action profiles.
-        final Set<PiActionProfileId> actionProfileIds = pipeconf.pipelineModel()
-                .actionProfiles()
-                .stream()
-                .map(PiActionProfileModel::id)
-                .collect(Collectors.toSet());
-        final Map<PiActionProfileGroupHandle, PiActionProfileGroup>
-                groupsOnDevice = dumpAllGroupsFromDevice(actionProfileIds);
+        final P4RuntimeReadClient.ReadRequest request = client.read(pipeconf);
+        pipeconf.pipelineModel().actionProfiles()
+                .stream().map(PiActionProfileModel::id)
+                .forEach(id -> request.actionProfileGroups(id)
+                        .actionProfileMembers(id));
+        final P4RuntimeReadClient.ReadResponse response = request.submitSync();
+
+        if (!response.isSuccess()) {
+            // Error at client level.
+            return Collections.emptyList();
+        }
+
+        final Collection<PiActionProfileGroup> groupsOnDevice = response.all(
+                PiActionProfileGroup.class);
         final Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice =
-                dumpAllMembersFromDevice(actionProfileIds);
+                response.all(PiActionProfileMember.class).stream()
+                        .collect(toMap(m -> m.handle(deviceId), m -> m));
 
         // Sync mirrors.
         groupMirror.sync(deviceId, groupsOnDevice);
-        memberMirror.sync(deviceId, membersOnDevice);
+        memberMirror.sync(deviceId, membersOnDevice.values());
 
         // Retrieve the original PD group before translation.
         final List<Group> result = Lists.newArrayList();
         final List<PiActionProfileGroup> groupsToRemove = Lists.newArrayList();
         final Set<PiActionProfileMemberHandle> memberHandlesToKeep = Sets.newHashSet();
-        for (PiActionProfileGroup piGroup : groupsOnDevice.values()) {
+        for (PiActionProfileGroup piGroup : groupsOnDevice) {
             final Group pdGroup = checkAndForgeGroupEntry(piGroup, membersOnDevice);
             if (pdGroup == null) {
                 // Entry is on device but unknown to translation service or
@@ -168,13 +168,24 @@
             }
         }
 
-        // Trigger clean up of inconsistent groups and members. This will update
-        // the mirror accordingly.
+        // Trigger clean up of inconsistent groups and members (if any). This
+        // process takes care of removing any orphan member, e.g. from a
+        // partial/unsuccessful group insertion.
+        // This will update the mirror accordingly.
         final Set<PiActionProfileMemberHandle> memberHandlesToRemove = Sets.difference(
                 membersOnDevice.keySet(), memberHandlesToKeep);
-        SharedExecutors.getSingleThreadExecutor().execute(
-                () -> cleanUpInconsistentGroupsAndMembers(
-                        groupsToRemove, memberHandlesToRemove));
+        final Set<PiActionProfileGroupHandle> groupHandlesToRemove = groupsToRemove
+                .stream().map(g -> g.handle(deviceId)).collect(toSet());
+        if (groupHandlesToRemove.size() + memberHandlesToRemove.size() > 0) {
+            log.warn("Cleaning up {} action profile groups and " +
+                             "{} members on {}...",
+                     groupHandlesToRemove.size(), memberHandlesToRemove.size(), deviceId);
+            SharedExecutors.getSingleThreadExecutor().execute(
+                    () -> submitWriteRequestAndUpdateMirror(
+                            client.write(pipeconf)
+                                    .delete(groupHandlesToRemove)
+                                    .delete(memberHandlesToRemove)));
+        }
 
         // Done.
         return result;
@@ -182,7 +193,9 @@
 
     private Collection<Group> getGroupsFromMirror() {
         final Map<PiActionProfileMemberHandle, PiActionProfileMember> members =
-                memberMirror.deviceHandleMap(deviceId);
+                memberMirror.getAll(deviceId).stream()
+                .map(TimedEntry::entry)
+                .collect(toMap(e -> e.handle(deviceId), e -> e));
         return groupMirror.getAll(deviceId).stream()
                 .map(TimedEntry::entry)
                 .map(g -> checkAndForgeGroupEntry(
@@ -191,60 +204,6 @@
                 .collect(Collectors.toList());
     }
 
-    private void cleanUpInconsistentGroupsAndMembers(Collection<PiActionProfileGroup> groupsToRemove,
-                                                     Collection<PiActionProfileMemberHandle> membersToRemove) {
-        if (!groupsToRemove.isEmpty()) {
-            log.warn("Found {} inconsistent action profile groups on {}, removing them...",
-                     groupsToRemove.size(), deviceId);
-            groupsToRemove.forEach(piGroup -> {
-                log.debug(piGroup.toString());
-                processPiGroup(piGroup, null, Operation.REMOVE);
-            });
-        }
-        if (!membersToRemove.isEmpty()) {
-            log.warn("Found {} inconsistent action profile members on {}, removing them...",
-                     membersToRemove.size(), deviceId);
-            // FIXME: implement client call to remove members from multiple
-            // action profiles in one shot.
-            final ListMultimap<PiActionProfileId, PiActionProfileMemberId>
-                    membersByActProfId = ArrayListMultimap.create();
-            membersToRemove.forEach(m -> membersByActProfId.put(
-                    m.actionProfileId(), m.memberId()));
-            membersByActProfId.keySet().forEach(actProfId -> {
-                List<PiActionProfileMemberId> removedMembers = getFutureWithDeadline(
-                        client.removeActionProfileMembers(
-                                actProfId, membersByActProfId.get(actProfId), pipeconf),
-                        "cleaning up action profile members", Collections.emptyList());
-                // Update member mirror.
-                removedMembers.stream()
-                        .map(id -> PiActionProfileMemberHandle.of(deviceId, actProfId, id))
-                        .forEach(memberMirror::remove);
-            });
-        }
-    }
-
-    private Map<PiActionProfileGroupHandle, PiActionProfileGroup> dumpAllGroupsFromDevice(
-            Set<PiActionProfileId> actProfIds) {
-        // TODO: implement P4Runtime client call to read all groups with one call
-        // Good if pipeline has multiple action profiles.
-        return actProfIds.stream()
-                .flatMap(actProfId -> getFutureWithDeadline(
-                        client.dumpActionProfileGroups(actProfId, pipeconf),
-                        "dumping groups", Collections.emptyList()).stream())
-                .collect(toMap(g -> PiActionProfileGroupHandle.of(deviceId, g), g -> g));
-    }
-
-    private Map<PiActionProfileMemberHandle, PiActionProfileMember> dumpAllMembersFromDevice(
-            Set<PiActionProfileId> actProfIds) {
-        // TODO: implement P4Runtime client call to read all members with one call
-        // Good if pipeline has multiple action profiles.
-        return actProfIds.stream()
-                .flatMap(actProfId -> getFutureWithDeadline(
-                        client.dumpActionProfileMembers(actProfId, pipeconf),
-                        "dumping members", Collections.emptyList()).stream())
-                .collect(toMap(m -> PiActionProfileMemberHandle.of(deviceId, m), m -> m));
-    }
-
     private Group checkAndForgeGroupEntry(
             PiActionProfileGroup piGroupOnDevice,
             Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
@@ -269,7 +228,7 @@
         // Groups in P4Runtime contains only a reference to members. Check that
         // the actual member instances in the translation store are the same
         // found on the device.
-        if (!validateMembers(piGroupFromStore, membersOnDevice)) {
+        if (!validateGroupMembers(piGroupFromStore, membersOnDevice)) {
             log.warn("Group on device {} refers to members that are different " +
                              "than those found in translation store: {}", handle);
             return null;
@@ -282,7 +241,7 @@
         return addedGroup(translatedEntity.get().original(), mirrorEntry.lifeSec());
     }
 
-    private boolean validateMembers(
+    private boolean validateGroupMembers(
             PiActionProfileGroup piGroupFromStore,
             Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
         final Collection<PiActionProfileMember> groupMembers =
@@ -291,9 +250,8 @@
             return false;
         }
         return groupMembers.stream().allMatch(
-                memberFromStore -> memberFromStore.equals(
-                        membersOnDevice.get(
-                                PiActionProfileMemberHandle.of(deviceId, memberFromStore))));
+                memberFromStore -> memberFromStore.equals(membersOnDevice.get(
+                        memberFromStore.handle(deviceId))));
     }
 
     private Group addedGroup(Group original, long life) {
@@ -314,143 +272,79 @@
         }
         final Operation operation = opType.equals(GroupOperation.Type.DELETE)
                 ? Operation.REMOVE : Operation.APPLY;
-        processPiGroup(piGroup, pdGroup, operation);
-    }
-
-    private void processPiGroup(PiActionProfileGroup groupToApply,
-                                Group pdGroup,
-                                Operation operation) {
-        final PiActionProfileGroupHandle handle = PiActionProfileGroupHandle.of(deviceId, groupToApply);
-        STRIPED_LOCKS.get(handle).lock();
-        try {
-            switch (operation) {
-                case APPLY:
-                    if (applyGroupWithMembersOrNothing(groupToApply, handle)) {
-                        groupTranslator.learn(handle, new PiTranslatedEntity<>(
-                                pdGroup, groupToApply, handle));
-                    }
-                    return;
-                case REMOVE:
-                    if (deleteGroup(groupToApply, handle)) {
-                        groupTranslator.forget(handle);
-                    }
-                    return;
-                default:
-                    log.error("Unknwon group operation type {}, cannot process group", operation);
-                    break;
+        final PiActionProfileGroupHandle handle = piGroup.handle(deviceId);
+        if (writePiGroupOnDevice(piGroup, handle, operation)) {
+            if (operation.equals(Operation.APPLY)) {
+                groupTranslator.learn(handle, new PiTranslatedEntity<>(
+                        pdGroup, piGroup, handle));
+            } else {
+                groupTranslator.forget(handle);
             }
-        } finally {
-            STRIPED_LOCKS.get(handle).unlock();
         }
     }
 
-    private boolean applyGroupWithMembersOrNothing(PiActionProfileGroup group, PiActionProfileGroupHandle handle) {
-        // First apply members, then group, if fails, delete members.
-        Collection<PiActionProfileMember> members = extractAllMemberInstancesOrNull(group);
+    private boolean writePiGroupOnDevice(
+            PiActionProfileGroup group,
+            PiActionProfileGroupHandle groupHandle,
+            Operation operation) {
+        // Generate a write request to write both members and groups. Return
+        // true if request is successful or if there's no need to write on
+        // device (according to mirror state), otherwise, return false.
+        final Collection<PiActionProfileMember> members = extractAllMemberInstancesOrNull(group);
         if (members == null) {
             return false;
         }
-        if (!applyAllMembersOrNothing(members)) {
-            return false;
-        }
-        if (!applyGroup(group, handle)) {
-            deleteMembers(handles(members));
-            return false;
-        }
-        return true;
-    }
-
-    private boolean applyGroup(PiActionProfileGroup groupToApply, PiActionProfileGroupHandle handle) {
-        final TimedEntry<PiActionProfileGroup> groupOnDevice = groupMirror.get(handle);
-        final P4RuntimeClient.WriteOperationType opType =
-                groupOnDevice == null ? INSERT : MODIFY;
-        if (opType.equals(MODIFY) && groupToApply.equals(groupOnDevice.entry())) {
-            // Skip writing, group is unchanged.
-            return true;
-        }
-        final boolean success = getFutureWithDeadline(
-                client.writeActionProfileGroup(groupToApply, opType, pipeconf),
-                "performing action profile group " + opType, false);
-        if (success) {
-            groupMirror.put(handle, groupToApply);
-        }
-        return success;
-    }
-
-    private boolean deleteGroup(PiActionProfileGroup group, PiActionProfileGroupHandle handle) {
-        final boolean success = getFutureWithDeadline(
-                client.writeActionProfileGroup(group, DELETE, pipeconf),
-                "performing action profile group " + DELETE, false);
-        if (success) {
-            groupMirror.remove(handle);
-        }
-        // Orphan members will be removed at the next reconciliation cycle.
-        return success;
-    }
-
-    private boolean applyAllMembersOrNothing(Collection<PiActionProfileMember> members) {
-        Collection<PiActionProfileMember> appliedMembers = applyMembers(members);
-        if (appliedMembers.size() == members.size()) {
+        final P4RuntimeWriteClient.WriteRequest request = client.write(pipeconf);
+        // FIXME: when operation is remove, should we remove members first? Same
+        //  thing when modifying a group, should we first modify the group then
+        //  remove the member?
+        final boolean allMembersSkipped = members.stream()
+                .allMatch(m -> appendEntityToWriteRequestOrSkip(
+                        request, m.handle(deviceId), m, memberMirror, operation));
+        final boolean groupSkipped = appendEntityToWriteRequestOrSkip(
+                request, groupHandle, group, groupMirror, operation);
+        if (allMembersSkipped && groupSkipped) {
             return true;
         } else {
-            deleteMembers(handles(appliedMembers));
-            return false;
+            // True if all entities in the request (groups and members) where
+            // written successfully.
+            return submitWriteRequestAndUpdateMirror(request).isSuccess();
         }
     }
 
-    private Collection<PiActionProfileMember> applyMembers(
-            Collection<PiActionProfileMember> members) {
-        return members.stream()
-                .filter(this::applyMember)
-                .collect(Collectors.toList());
-    }
-
-    private boolean applyMember(PiActionProfileMember memberToApply) {
-        // If exists, modify, otherwise insert.
-        final PiActionProfileMemberHandle handle = PiActionProfileMemberHandle.of(
-                deviceId, memberToApply);
-        final TimedEntry<PiActionProfileMember> memberOnDevice = memberMirror.get(handle);
-        final P4RuntimeClient.WriteOperationType opType =
-                memberOnDevice == null ? INSERT : MODIFY;
-        if (opType.equals(MODIFY) && memberToApply.equals(memberOnDevice.entry())) {
-            // Skip writing if member is unchanged.
-            return true;
+    private <H extends PiHandle, E extends PiEntity> boolean appendEntityToWriteRequestOrSkip(
+            P4RuntimeWriteClient.WriteRequest writeRequest,
+            H handle,
+            E entityToApply,
+            P4RuntimeMirror<H, E> mirror,
+            Operation operation) {
+        // Should return true if there's no need to write entity on device,
+        // false if the write request is modified or an error occurs.
+        final TimedEntry<E> entityOnDevice = mirror.get(handle);
+        switch (operation) {
+            case APPLY:
+                if (entityOnDevice == null) {
+                    writeRequest.insert(entityToApply);
+                } else if (entityToApply.equals(entityOnDevice.entry())) {
+                    // Skip writing if group is unchanged.
+                    return true;
+                } else {
+                    writeRequest.modify(entityToApply);
+                }
+                break;
+            case REMOVE:
+                if (entityOnDevice == null) {
+                    // Skip deleting if group does not exist on device.
+                    return true;
+                } else {
+                    writeRequest.delete(handle);
+                }
+                break;
+            default:
+                log.error("Unrecognized operation {}", operation);
+                break;
         }
-        final boolean success = getFutureWithDeadline(
-                client.writeActionProfileMembers(
-                        singletonList(memberToApply), opType, pipeconf),
-                "performing action profile member " + opType, false);
-        if (success) {
-            memberMirror.put(handle, memberToApply);
-        }
-        return success;
-    }
-
-    private void deleteMembers(Collection<PiActionProfileMemberHandle> handles) {
-        // TODO: improve by batching deletes.
-        handles.forEach(this::deleteMember);
-    }
-
-    private void deleteMember(PiActionProfileMemberHandle handle) {
-        final boolean success = getFutureWithDeadline(
-                client.removeActionProfileMembers(
-                        handle.actionProfileId(),
-                        singletonList(handle.memberId()), pipeconf),
-                "performing action profile member " + DELETE,
-                Collections.emptyList())
-                // Successful if the only member passed has been removed.
-                .size() == 1;
-        if (success) {
-            memberMirror.remove(handle);
-        }
-    }
-
-    private Collection<PiActionProfileMemberHandle> handles(
-            Collection<PiActionProfileMember> members) {
-        return members.stream()
-                .map(m -> PiActionProfileMemberHandle.of(
-                        deviceId, m.actionProfile(), m.id()))
-                .collect(Collectors.toList());
+        return false;
     }
 
     private Collection<PiActionProfileMember> extractAllMemberInstancesOrNull(
@@ -468,6 +362,14 @@
         return instances;
     }
 
+    private P4RuntimeWriteClient.WriteResponse submitWriteRequestAndUpdateMirror(
+            P4RuntimeWriteClient.WriteRequest request) {
+        final P4RuntimeWriteClient.WriteResponse response = request.submitSync();
+        groupMirror.replayWriteResponse(response);
+        memberMirror.replayWriteResponse(response);
+        return response;
+    }
+
     enum Operation {
         APPLY, REMOVE
     }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
index 693353f..f905c4c 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.Striped;
 import org.onlab.util.SharedExecutors;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeTableMirror;
 import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
@@ -27,19 +26,24 @@
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleProgrammable;
+import org.onosproject.net.pi.model.PiCounterType;
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.model.PiPipelineModel;
 import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.model.PiTableModel;
 import org.onosproject.net.pi.runtime.PiCounterCell;
 import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.net.pi.runtime.PiEntityType;
+import org.onosproject.net.pi.runtime.PiHandle;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableEntryHandle;
 import org.onosproject.net.pi.service.PiFlowRuleTranslator;
 import org.onosproject.net.pi.service.PiTranslatedEntity;
 import org.onosproject.net.pi.service.PiTranslationException;
-import org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType;
+import org.onosproject.p4runtime.api.P4RuntimeReadClient;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -48,18 +52,14 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.locks.Lock;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import static com.google.common.collect.Lists.newArrayList;
 import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.APPLY;
 import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.REMOVE;
 import static org.onosproject.net.flow.FlowEntry.FlowEntryState.ADDED;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.DELETE;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.INSERT;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.MODIFY;
 
 /**
  * Implementation of the flow rule programmable behaviour for P4Runtime.
@@ -75,11 +75,6 @@
     private static final String DELETE_BEFORE_UPDATE = "tableDeleteBeforeUpdate";
     private static final boolean DEFAULT_DELETE_BEFORE_UPDATE = false;
 
-    // If true, we ignore re-installing rules that already exist in the
-    // device mirror, i.e. same match key and action.
-    private static final String IGNORE_SAME_ENTRY_UPDATE = "tableIgnoreSameEntryUpdate";
-    private static final boolean DEFAULT_IGNORE_SAME_ENTRY_UPDATE = false;
-
     // If true, we avoid querying the device and return what's already known by
     // the ONOS store.
     private static final String READ_FROM_MIRROR = "tableReadFromMirror";
@@ -102,9 +97,6 @@
     private static final String TABLE_DEFAULT_AS_ENTRY = "tableDefaultAsEntry";
     private static final boolean DEFAULT_TABLE_DEFAULT_AS_ENTRY = false;
 
-    // Needed to synchronize operations over the same table entry.
-    private static final Striped<Lock> ENTRY_LOCKS = Striped.lock(30);
-
     private PiPipelineModel pipelineModel;
     private P4RuntimeTableMirror tableMirror;
     private PiFlowRuleTranslator translator;
@@ -136,24 +128,21 @@
         final ImmutableList.Builder<FlowEntry> result = ImmutableList.builder();
         final List<PiTableEntry> inconsistentEntries = Lists.newArrayList();
 
-        // Read table entries, including default ones.
-        final Collection<PiTableEntry> deviceEntries = Stream.concat(
-                streamEntries(), streamDefaultEntries())
-                // Ignore entries from constant tables.
-                .filter(e -> !tableIsConstant(e.table()))
-                // Device implementation might return duplicate entries. For
-                // example if reading only default ones is not supported and
-                // non-default entries are returned, by using distinct() we are
-                // robust against that possibility.
-                .distinct()
-                .collect(Collectors.toList());
-
-        if (deviceEntries.isEmpty()) {
+        // Read table entries from device.
+        final Collection<PiTableEntry> deviceEntries = getAllTableEntriesFromDevice();
+        if (deviceEntries == null) {
+            // Potential error at the client level.
             return Collections.emptyList();
         }
 
         // Synchronize mirror with the device state.
-        syncMirror(deviceEntries);
+        tableMirror.sync(deviceId, deviceEntries);
+
+        if (deviceEntries.isEmpty()) {
+            // Nothing to do.
+            return Collections.emptyList();
+        }
+
         final Map<PiTableEntry, PiCounterCellData> counterCellMap =
                 readEntryCounters(deviceEntries);
         // Forge flow entries with counter values.
@@ -174,7 +163,7 @@
             }
         }
 
-        if (inconsistentEntries.size() > 0) {
+        if (!inconsistentEntries.isEmpty()) {
             // Trigger clean up of inconsistent entries.
             SharedExecutors.getSingleThreadExecutor().execute(
                     () -> cleanUpInconsistentEntries(inconsistentEntries));
@@ -183,32 +172,28 @@
         return result.build();
     }
 
-    private Stream<PiTableEntry> streamEntries() {
-        return getFutureWithDeadline(
-                client.dumpAllTables(pipeconf), "dumping all tables",
-                Collections.emptyList())
-                .stream();
-    }
-
-    private Stream<PiTableEntry> streamDefaultEntries() {
-        // Ignore tables with constant default action.
-        final Set<PiTableId> defaultTables = pipelineModel.tables()
-                .stream()
-                .filter(table -> !table.constDefaultAction().isPresent())
-                .map(PiTableModel::id)
-                .collect(Collectors.toSet());
-        return defaultTables.isEmpty() ? Stream.empty()
-                : getFutureWithDeadline(
-                client.dumpTables(defaultTables, true, pipeconf),
-                "dumping default table entries",
-                Collections.emptyList())
-                .stream();
-    }
-
-    private void syncMirror(Collection<PiTableEntry> entries) {
-        Map<PiTableEntryHandle, PiTableEntry> handleMap = Maps.newHashMap();
-        entries.forEach(e -> handleMap.put(PiTableEntryHandle.of(deviceId, e), e));
-        tableMirror.sync(deviceId, handleMap);
+    private Collection<PiTableEntry> getAllTableEntriesFromDevice() {
+        final P4RuntimeReadClient.ReadRequest request = client.read(pipeconf);
+        // Read entries from all non-constant tables, including default ones.
+        pipelineModel.tables().stream()
+                .filter(t -> !t.isConstantTable())
+                .forEach(t -> {
+                    request.tableEntries(t.id());
+                    if (!t.constDefaultAction().isPresent()) {
+                        request.defaultTableEntry(t.id());
+                    }
+                });
+        final P4RuntimeReadClient.ReadResponse response = request.submitSync();
+        if (!response.isSuccess()) {
+            return null;
+        }
+        return response.all(PiTableEntry.class).stream()
+                // Device implementation might return duplicate entries. For
+                // example if reading only default ones is not supported and
+                // non-default entries are returned, by using distinct() we
+                // are robust against that possibility.
+                .distinct()
+                .collect(Collectors.toList());
     }
 
     @Override
@@ -223,8 +208,7 @@
 
     private FlowEntry forgeFlowEntry(PiTableEntry entry,
                                      PiCounterCellData cellData) {
-        final PiTableEntryHandle handle = PiTableEntryHandle
-                .of(deviceId, entry);
+        final PiTableEntryHandle handle = entry.handle(deviceId);
         final Optional<PiTranslatedEntity<FlowRule, PiTableEntry>>
                 translatedEntity = translator.lookup(handle);
         final TimedEntry<PiTableEntry> timedEntry = tableMirror.get(handle);
@@ -265,105 +249,157 @@
     private void cleanUpInconsistentEntries(Collection<PiTableEntry> piEntries) {
         log.warn("Found {} inconsistent table entries on {}, removing them...",
                  piEntries.size(), deviceId);
-        piEntries.forEach(entry -> {
-            log.debug(entry.toString());
-            final PiTableEntryHandle handle = PiTableEntryHandle.of(deviceId, entry);
-            ENTRY_LOCKS.get(handle).lock();
-            try {
-                applyEntry(handle, entry, null, REMOVE);
-            } finally {
-                ENTRY_LOCKS.get(handle).unlock();
-            }
-        });
+        // Remove entries and update mirror.
+        tableMirror.replayWriteResponse(
+                client.write(pipeconf).entities(piEntries, DELETE).submitSync());
     }
 
     private Collection<FlowRule> processFlowRules(Collection<FlowRule> rules,
                                                   Operation driverOperation) {
-
         if (!setupBehaviour() || rules.isEmpty()) {
             return Collections.emptyList();
         }
-
-        final ImmutableList.Builder<FlowRule> result = ImmutableList.builder();
-
-        // TODO: send writes in bulk (e.g. all entries to insert, modify or delete).
-        // Instead of calling the client for each one of them.
-
-        for (FlowRule ruleToApply : rules) {
-
-            final PiTableEntry piEntryToApply;
+        // Created batched write request.
+        final P4RuntimeWriteClient.WriteRequest request = client.write(pipeconf);
+        // For each rule, translate to PI and append to write request.
+        final Map<PiHandle, FlowRule> handleToRuleMap = Maps.newHashMap();
+        final List<FlowRule> skippedRules = Lists.newArrayList();
+        for (FlowRule rule : rules) {
+            final PiTableEntry entry;
             try {
-                piEntryToApply = translator.translate(ruleToApply, pipeconf);
+                entry = translator.translate(rule, pipeconf);
             } catch (PiTranslationException e) {
-                log.warn("Unable to translate flow rule for pipeconf '{}': {} - {}",
-                         pipeconf.id(), e.getMessage(), ruleToApply);
+                log.warn("Unable to translate flow rule for pipeconf '{}': {} [{}]",
+                         pipeconf.id(), e.getMessage(), rule);
                 // Next rule.
                 continue;
             }
-
-            final PiTableEntryHandle handle = PiTableEntryHandle
-                    .of(deviceId, piEntryToApply);
-
-            // Serialize operations over the same match key/table/device ID.
-            ENTRY_LOCKS.get(handle).lock();
-            try {
-                if (applyEntry(handle, piEntryToApply,
-                               ruleToApply, driverOperation)) {
-                    result.add(ruleToApply);
-                }
-            } finally {
-                ENTRY_LOCKS.get(handle).unlock();
+            final PiTableEntryHandle handle = entry.handle(deviceId);
+            handleToRuleMap.put(handle, rule);
+            // Append entry to batched write request (returns false), or skip (true)
+            if (appendEntryToWriteRequestOrSkip(
+                    request, handle, entry, driverOperation)) {
+                skippedRules.add(rule);
+                updateTranslationStore(
+                        driverOperation, handle, rule, entry);
             }
         }
-
-        return result.build();
+        // Submit request to server.
+        final P4RuntimeWriteClient.WriteResponse response = request.submitSync();
+        // Update mirror.
+        tableMirror.replayWriteResponse(response);
+        // Derive successfully applied flow rule from response.
+        final List<FlowRule> appliedRules = getAppliedFlowRulesAndUpdateTranslator(
+                response, handleToRuleMap, driverOperation);
+        // Return skipped and applied rules.
+        return ImmutableList.<FlowRule>builder()
+                .addAll(skippedRules).addAll(appliedRules).build();
     }
 
-    /**
-     * Applies the given entry to the device, and returns true if the operation
-     * was successful, false otherwise.
-     */
-    private boolean applyEntry(final PiTableEntryHandle handle,
-                               PiTableEntry piEntryToApply,
-                               final FlowRule ruleToApply,
-                               final Operation driverOperation) {
+    private List<FlowRule> getAppliedFlowRulesAndUpdateTranslator(
+            P4RuntimeWriteClient.WriteResponse response,
+            Map<PiHandle, FlowRule> handleToFlowRuleMap,
+            Operation driverOperation) {
+        // Returns a list of flow rules that were successfully written on the
+        // server according to the given write response and operation.
+        return response.success().stream()
+                .filter(r -> r.entityType().equals(PiEntityType.TABLE_ENTRY))
+                .map(r -> {
+                    final PiHandle handle = r.handle();
+                    final FlowRule rule = handleToFlowRuleMap.get(handle);
+                    if (rule == null) {
+                        log.error("Server returned unrecognized table entry " +
+                                          "handle in write response: {}", handle);
+                        return null;
+                    }
+                    // Filter intermediate responses (e.g. P4Runtime DELETE
+                    // during FlowRule APPLY because we are performing
+                    // delete-before-update)
+                    if (isUpdateTypeRelevant(r.updateType(), driverOperation)) {
+                        updateTranslationStore(
+                                driverOperation, (PiTableEntryHandle) handle,
+                                rule, (PiTableEntry) r.entity());
+                        return rule;
+                    }
+                    return null;
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    private boolean isUpdateTypeRelevant(UpdateType p4UpdateType, Operation driverOperation) {
+        switch (p4UpdateType) {
+            case INSERT:
+            case MODIFY:
+                if (!driverOperation.equals(APPLY)) {
+                    return false;
+                }
+                break;
+            case DELETE:
+                if (!driverOperation.equals(REMOVE)) {
+                    return false;
+                }
+                break;
+            default:
+                log.error("Unknown update type {}", p4UpdateType);
+                return false;
+        }
+        return true;
+    }
+
+    private void updateTranslationStore(
+            Operation operation, PiTableEntryHandle handle,
+            FlowRule rule, PiTableEntry entry) {
+        if (operation.equals(APPLY)) {
+            translator.learn(handle, new PiTranslatedEntity<>(
+                    rule, entry, handle));
+        } else {
+            translator.forget(handle);
+        }
+    }
+
+    private boolean appendEntryToWriteRequestOrSkip(
+            final P4RuntimeWriteClient.WriteRequest writeRequest,
+            final PiTableEntryHandle handle,
+            PiTableEntry piEntryToApply,
+            final Operation driverOperation) {
         // Depending on the driver operation, and if a matching rule exists on
-        // the device, decide which P4 Runtime write operation to perform for
-        // this entry.
+        // the device/mirror, decide which P4Runtime update operation to perform
+        // for this entry. In some cases, the entry is skipped from the write
+        // request but we want to return the corresponding flow rule as
+        // successfully written. In this case, we return true.
         final TimedEntry<PiTableEntry> piEntryOnDevice = tableMirror.get(handle);
-        final WriteOperationType p4Operation;
-        final WriteOperationType storeOperation;
+        final UpdateType updateType;
 
         final boolean defaultAsEntry = driverBoolProperty(
                 TABLE_DEFAULT_AS_ENTRY, DEFAULT_TABLE_DEFAULT_AS_ENTRY);
-        final boolean ignoreSameEntryUpdate = driverBoolProperty(
-                IGNORE_SAME_ENTRY_UPDATE, DEFAULT_IGNORE_SAME_ENTRY_UPDATE);
         final boolean deleteBeforeUpdate = driverBoolProperty(
                 DELETE_BEFORE_UPDATE, DEFAULT_DELETE_BEFORE_UPDATE);
+
         if (driverOperation == APPLY) {
             if (piEntryOnDevice == null) {
                 // Entry is first-timer, INSERT or MODIFY if default action.
-                p4Operation = !piEntryToApply.isDefaultAction() || defaultAsEntry
+                updateType = !piEntryToApply.isDefaultAction() || defaultAsEntry
                         ? INSERT : MODIFY;
-                storeOperation = p4Operation;
             } else {
-                if (ignoreSameEntryUpdate &&
-                        piEntryToApply.action().equals(piEntryOnDevice.entry().action())) {
+                if (piEntryToApply.action().equals(piEntryOnDevice.entry().action())) {
+                    // FIXME: should we check for other attributes of the table
+                    //  entry? For example can we modify the priority?
                     log.debug("Ignoring re-apply of existing entry: {}", piEntryToApply);
-                    p4Operation = null;
+                    return true;
                 } else if (deleteBeforeUpdate && !piEntryToApply.isDefaultAction()) {
-                    // Some devices return error when updating existing
-                    // entries. If requested, remove entry before
-                    // re-inserting the modified one, except the default action
-                    // entry, that cannot be removed.
-                    applyEntry(handle, piEntryOnDevice.entry(), null, REMOVE);
-                    p4Operation = INSERT;
+                    // Some devices return error when updating existing entries.
+                    // If requested, remove entry before re-inserting the
+                    // modified one, except the default action entry, that
+                    // cannot be removed.
+                    writeRequest.delete(handle);
+                    updateType = INSERT;
                 } else {
-                    p4Operation = MODIFY;
+                    updateType = MODIFY;
                 }
-                storeOperation = p4Operation;
             }
         } else {
+            // REMOVE.
             if (piEntryToApply.isDefaultAction()) {
                 // Cannot remove default action. Instead we should use the
                 // original defined by the interpreter (if any).
@@ -371,26 +407,13 @@
                 if (piEntryToApply == null) {
                     return false;
                 }
-                p4Operation = MODIFY;
+                updateType = MODIFY;
             } else {
-                p4Operation = DELETE;
+                updateType = DELETE;
             }
-            // Still want to delete the default entry from the mirror and
-            // translation store.
-            storeOperation = DELETE;
         }
-
-        if (p4Operation != null) {
-            if (writeEntry(piEntryToApply, p4Operation)) {
-                updateStores(handle, piEntryToApply, ruleToApply, storeOperation);
-                return true;
-            } else {
-                return false;
-            }
-        } else {
-            // If no operation, let's pretend we applied the rule to the device.
-            return true;
-        }
+        writeRequest.entity(piEntryToApply, updateType);
+        return false;
     }
 
     private PiTableEntry getOriginalDefaultEntry(PiTableId tableId) {
@@ -421,38 +444,6 @@
                 originalDefaultEntry.action().equals(entry.action());
     }
 
-    /**
-     * Performs a write operation on the device.
-     */
-    private boolean writeEntry(PiTableEntry entry,
-                               WriteOperationType p4Operation) {
-        final CompletableFuture<Boolean> future = client.writeTableEntries(
-                newArrayList(entry), p4Operation, pipeconf);
-        // If false, errors logged by internal calls.
-        return getFutureWithDeadline(
-                future, "performing table " + p4Operation.name(), false);
-    }
-
-    private void updateStores(PiTableEntryHandle handle,
-                              PiTableEntry entry,
-                              FlowRule rule,
-                              WriteOperationType p4Operation) {
-        switch (p4Operation) {
-            case INSERT:
-            case MODIFY:
-                tableMirror.put(handle, entry);
-                translator.learn(handle, new PiTranslatedEntity<>(rule, entry, handle));
-                break;
-            case DELETE:
-                tableMirror.remove(handle);
-                translator.forget(handle);
-                break;
-            default:
-                throw new IllegalArgumentException(
-                        "Unknown operation " + p4Operation.name());
-        }
-    }
-
     private Map<PiTableEntry, PiCounterCellData> readEntryCounters(
             Collection<PiTableEntry> tableEntries) {
         if (!driverBoolProperty(SUPPORT_TABLE_COUNTERS,
@@ -461,22 +452,44 @@
             return Collections.emptyMap();
         }
 
-        if (driverBoolProperty(READ_COUNTERS_WITH_TABLE_ENTRIES,
-                               DEFAULT_READ_COUNTERS_WITH_TABLE_ENTRIES)) {
-            return tableEntries.stream().collect(Collectors.toMap(c -> c, PiTableEntry::counter));
-        } else {
-            Collection<PiCounterCell> cells;
-            Set<PiCounterCellId> cellIds = tableEntries.stream()
-                    // Ignore counter for default entry.
-                    .filter(e -> !e.isDefaultAction())
-                    .filter(e -> tableHasCounter(e.table()))
-                    .map(PiCounterCellId::ofDirect)
-                    .collect(Collectors.toSet());
-            cells = getFutureWithDeadline(client.readCounterCells(cellIds, pipeconf),
-                                              "reading table counters", Collections.emptyList());
-            return cells.stream()
-                    .collect(Collectors.toMap(c -> c.cellId().tableEntry(), PiCounterCell::data));
+        final Map<PiTableEntry, PiCounterCellData> cellDataMap = Maps.newHashMap();
+
+        // We expect the server to return table entries with counter data (if
+        // the table supports counter). Here we extract such counter data and we
+        // determine if there are missing counter cells (if, for example, the
+        // serves does not support returning counter data with table entries)
+        final Set<PiHandle> missingCellHandles = tableEntries.stream()
+                .map(t -> {
+                    if (t.counter() != null) {
+                        // Counter data found in table entry.
+                        cellDataMap.put(t, t.counter());
+                        return null;
+                    } else {
+                        return t;
+                    }
+                })
+                .filter(Objects::nonNull)
+                // Ignore for default entries and for tables without counters.
+                .filter(e -> !e.isDefaultAction())
+                .filter(e -> tableHasCounter(e.table()))
+                .map(PiCounterCellId::ofDirect)
+                .map(id -> PiCounterCellHandle.of(deviceId, id))
+                .collect(Collectors.toSet());
+        // We might be sending a large read request (for thousands or more
+        // of counter cell handles). We request the driver to vet this
+        // operation via driver property.
+        if (!missingCellHandles.isEmpty()
+                && !driverBoolProperty(READ_COUNTERS_WITH_TABLE_ENTRIES,
+                                       DEFAULT_READ_COUNTERS_WITH_TABLE_ENTRIES)) {
+            client.read(pipeconf)
+                    .handles(missingCellHandles)
+                    .submitSync()
+                    .all(PiCounterCell.class).stream()
+                    .filter(c -> c.cellId().counterType().equals(PiCounterType.DIRECT))
+                    .forEach(c -> cellDataMap.put(c.cellId().tableEntry(), c.data()));
         }
+
+        return cellDataMap;
     }
 
     private boolean tableHasCounter(PiTableId tableId) {
@@ -484,11 +497,6 @@
                 !pipelineModel.table(tableId).get().counters().isEmpty();
     }
 
-    private boolean tableIsConstant(PiTableId tableId) {
-        return pipelineModel.table(tableId).isPresent() &&
-                pipelineModel.table(tableId).get().isConstantTable();
-    }
-
     enum Operation {
         APPLY, REMOVE
     }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
index 15836b1..4d84b2a 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
@@ -35,11 +35,12 @@
     public CompletableFuture<Boolean> connect() {
         return CompletableFuture
                 .supplyAsync(super::createClient)
-                .thenComposeAsync(client -> {
+                .thenApplyAsync(client -> {
                     if (client == null) {
-                        return CompletableFuture.completedFuture(false);
+                        return false;
                     }
-                    return client.startStreamChannel();
+                    client.openSession();
+                    return true;
                 });
     }
 
@@ -48,7 +49,7 @@
         final P4RuntimeController controller = handler().get(P4RuntimeController.class);
         final DeviceId deviceId = handler().data().deviceId();
         final P4RuntimeClient client = controller.getClient(deviceId);
-        return client != null && client.isStreamChannelOpen();
+        return client != null && client.isSessionOpen();
     }
 
     @Override
@@ -85,12 +86,7 @@
     @Override
     public void roleChanged(MastershipRole newRole) {
         if (setupBehaviour() && newRole.equals(MastershipRole.MASTER)) {
-            client.becomeMaster().thenAcceptAsync(result -> {
-                if (!result) {
-                    log.error("Unable to notify mastership role {} to {}",
-                              newRole, deviceId);
-                }
-            });
+            client.runForMastership();
         }
     }
 
@@ -99,7 +95,7 @@
         final P4RuntimeController controller = handler().get(P4RuntimeController.class);
         final DeviceId deviceId = handler().data().deviceId();
         final P4RuntimeClient client = controller.getClient(deviceId);
-        if (client == null || !client.isStreamChannelOpen()) {
+        if (client == null || !client.isSessionOpen()) {
             return MastershipRole.NONE;
         }
         return client.isMaster() ? MastershipRole.MASTER : MastershipRole.STANDBY;
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMeterProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMeterProgrammable.java
index f2dff80..c9c436d 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMeterProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMeterProgrammable.java
@@ -31,7 +31,7 @@
 import org.onosproject.net.pi.model.PiMeterModel;
 import org.onosproject.net.pi.model.PiPipelineModel;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
 import org.onosproject.net.pi.service.PiMeterTranslator;
 import org.onosproject.net.pi.service.PiTranslationException;
 
@@ -40,26 +40,23 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
-import static com.google.common.collect.Lists.newArrayList;
-
 /**
  * Implementation of MeterProgrammable behaviour for P4Runtime.
  */
 public class P4RuntimeMeterProgrammable extends AbstractP4RuntimeHandlerBehaviour implements MeterProgrammable {
 
     private static final int METER_LOCK_EXPIRE_TIME_IN_MIN = 10;
-    private static final LoadingCache<PiMeterHandle, Lock>
+    private static final LoadingCache<PiMeterCellHandle, Lock>
             ENTRY_LOCKS = CacheBuilder.newBuilder()
             .expireAfterAccess(METER_LOCK_EXPIRE_TIME_IN_MIN, TimeUnit.MINUTES)
-            .build(new CacheLoader<PiMeterHandle, Lock>() {
+            .build(new CacheLoader<PiMeterCellHandle, Lock>() {
                 @Override
-                public Lock load(PiMeterHandle handle) {
+                public Lock load(PiMeterCellHandle handle) {
                     return new ReentrantLock();
                 }
             });
@@ -93,7 +90,7 @@
     private boolean processMeterOp(MeterOperation meterOp) {
 
         if (meterOp.type() != MeterOperation.Type.MODIFY) {
-            log.warn("P4runtime meter operations must be MODIFY!");
+            log.warn("P4Runtime meter operations must be MODIFY!");
             return false;
         }
 
@@ -106,11 +103,10 @@
             return false;
         }
 
-        final PiMeterHandle handle = PiMeterHandle.of(deviceId, piMeterCellConfig);
+        final PiMeterCellHandle handle = PiMeterCellHandle.of(deviceId, piMeterCellConfig);
         ENTRY_LOCKS.getUnchecked(handle).lock();
-        final boolean result = getFutureWithDeadline(
-                client.writeMeterCells(newArrayList(piMeterCellConfig), pipeconf),
-                "writing meter cells", false);
+        final boolean result = client.write(pipeconf)
+                .modify(piMeterCellConfig).submitSync().isSuccess();
         if (result) {
             meterMirror.put(handle, piMeterCellConfig);
         }
@@ -133,13 +129,8 @@
             meterIds.add(mode.id());
         }
 
-        try {
-            piMeterCellConfigs = client.readAllMeterCells(meterIds, pipeconf).get();
-        } catch (InterruptedException | ExecutionException e) {
-            log.warn("Exception while reading meters from {}: {}", deviceId, e.toString());
-            log.debug("", e);
-            return CompletableFuture.completedFuture(Collections.emptyList());
-        }
+        piMeterCellConfigs = client.read(pipeconf)
+                .meterCells(meterIds).submitSync().all(PiMeterCellConfig.class);
 
         Collection<Meter> meters = piMeterCellConfigs.stream()
                 .map(p -> {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMulticastGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMulticastGroupProgrammable.java
index ecdda08..fcb578d 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMulticastGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeMulticastGroupProgrammable.java
@@ -42,9 +42,9 @@
 import java.util.concurrent.locks.Lock;
 import java.util.stream.Collectors;
 
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
-import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.DELETE;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.INSERT;
+import static org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType.MODIFY;
 
 /**
  * Implementation of GroupProgrammable to handle multicast groups in P4Runtime.
@@ -52,6 +52,8 @@
 public class P4RuntimeMulticastGroupProgrammable
         extends AbstractP4RuntimeHandlerBehaviour implements GroupProgrammable {
 
+    // TODO: implement reading groups from device and mirror sync.
+
     // Needed to synchronize operations over the same group.
     private static final Striped<Lock> STRIPED_LOCKS = Striped.lock(30);
 
@@ -92,7 +94,7 @@
     }
 
     private Collection<Group> getMcGroups() {
-        // TODO: missing support for reading multicast groups is ready in PI/Stratum.
+        // TODO: missing support for reading multicast groups in PI/Stratum.
         return getMcGroupsFromMirror();
     }
 
@@ -160,17 +162,15 @@
         }
     }
 
-    private boolean writeMcGroupOnDevice(PiMulticastGroupEntry group, P4RuntimeClient.WriteOperationType opType) {
-        return getFutureWithDeadline(
-                client.writePreMulticastGroupEntries(
-                        Collections.singletonList(group), opType),
-                "performing multicast group " + opType, false);
+    private boolean writeMcGroupOnDevice(
+            PiMulticastGroupEntry group, P4RuntimeClient.UpdateType opType) {
+        return client.write(pipeconf).entity(group, opType).submitSync().isSuccess();
     }
 
     private boolean mcGroupApply(PiMulticastGroupEntryHandle handle,
                                  PiMulticastGroupEntry piGroup,
                                  Group pdGroup,
-                                 P4RuntimeClient.WriteOperationType opType) {
+                                 P4RuntimeClient.UpdateType opType) {
         switch (opType) {
             case DELETE:
                 if (writeMcGroupOnDevice(piGroup, DELETE)) {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimePacketProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimePacketProgrammable.java
index 7122784..73acf98 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimePacketProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimePacketProgrammable.java
@@ -47,9 +47,7 @@
             Collection<PiPacketOperation> operations = interpreter.mapOutboundPacket(packet);
             operations.forEach(piPacketOperation -> {
                 log.debug("Doing PiPacketOperation {}", piPacketOperation);
-                getFutureWithDeadline(
-                        client.packetOut(piPacketOperation, pipeconf),
-                        "sending packet-out", false);
+                client.packetOut(piPacketOperation, pipeconf);
             });
         } catch (PiPipelineInterpreter.PiInterpreterException e) {
             log.error("Unable to translate outbound packet for {} with pipeconf {}: {}",
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
index e1fc1e9..fb9451c 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
@@ -23,10 +23,13 @@
 import org.onosproject.net.Annotations;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.onosproject.net.pi.runtime.PiHandle;
 import org.onosproject.net.pi.service.PiPipeconfWatchdogEvent;
 import org.onosproject.net.pi.service.PiPipeconfWatchdogListener;
 import org.onosproject.net.pi.service.PiPipeconfWatchdogService;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
+import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.EventuallyConsistentMap;
 import org.onosproject.store.service.StorageService;
 import org.onosproject.store.service.WallClockTimestamp;
@@ -66,26 +69,38 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
     protected PiPipeconfWatchdogService pipeconfWatchdogService;
 
-    private EventuallyConsistentMap<H, TimedEntry<E>> mirrorMap;
+    private EventuallyConsistentMap<PiHandle, TimedEntry<E>> mirrorMap;
+    private EventuallyConsistentMap<PiHandle, Annotations> annotationsMap;
 
-    private EventuallyConsistentMap<H, Annotations> annotationsMap;
+    private final PiEntityType entityType;
 
     private final PiPipeconfWatchdogListener pipeconfListener =
             new InternalPipeconfWatchdogListener();
 
+    AbstractDistributedP4RuntimeMirror(PiEntityType entityType) {
+        this.entityType = entityType;
+    }
+
     @Activate
     public void activate() {
+        final String mapName = "onos-p4runtime-mirror-"
+                + entityType.name().toLowerCase();
+        final KryoNamespace serializer = KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(TimedEntry.class)
+                .build();
+
         mirrorMap = storageService
-                .<H, TimedEntry<E>>eventuallyConsistentMapBuilder()
-                .withName(mapName())
-                .withSerializer(storeSerializer())
+                .<PiHandle, TimedEntry<E>>eventuallyConsistentMapBuilder()
+                .withName(mapName)
+                .withSerializer(serializer)
                 .withTimestampProvider((k, v) -> new WallClockTimestamp())
                 .build();
 
         annotationsMap = storageService
-                .<H, Annotations>eventuallyConsistentMapBuilder()
-                .withName(mapName() + "-annotations")
-                .withSerializer(storeSerializer())
+                .<PiHandle, Annotations>eventuallyConsistentMapBuilder()
+                .withName(mapName + "-annotations")
+                .withSerializer(serializer)
                 .withTimestampProvider((k, v) -> new WallClockTimestamp())
                 .build();
 
@@ -93,10 +108,6 @@
         log.info("Started");
     }
 
-    abstract String mapName();
-
-    abstract KryoNamespace storeSerializer();
-
     @Deactivate
     public void deactivate() {
         pipeconfWatchdogService.removeListener(pipeconfListener);
@@ -158,9 +169,12 @@
     }
 
     @Override
-    public void sync(DeviceId deviceId, Map<H, E> deviceState) {
+    @SuppressWarnings("unchecked")
+    public void sync(DeviceId deviceId, Collection<E> entities) {
         checkNotNull(deviceId);
-        final Map<H, E> localState = deviceHandleMap(deviceId);
+        final Map<PiHandle, E> deviceState = entities.stream()
+                .collect(Collectors.toMap(e -> e.handle(deviceId), e -> e));
+        final Map<PiHandle, E> localState = deviceHandleMap(deviceId);
 
         final AtomicInteger removeCount = new AtomicInteger(0);
         final AtomicInteger updateCount = new AtomicInteger(0);
@@ -172,7 +186,7 @@
                     final E entryToAdd = deviceState.get(deviceHandle);
                     log.debug("Adding mirror entry for {}: {}",
                               deviceId, entryToAdd);
-                    put(deviceHandle, entryToAdd);
+                    put((H) deviceHandle, entryToAdd);
                     addCount.incrementAndGet();
                 });
         // Update or remove local entries.
@@ -181,12 +195,12 @@
             final E deviceEntry = deviceState.get(localHandle);
             if (deviceEntry == null) {
                 log.debug("Removing mirror entry for {}: {}", deviceId, localEntry);
-                remove(localHandle);
+                remove((H) localHandle);
                 removeCount.incrementAndGet();
             } else if (!deviceEntry.equals(localEntry)) {
                 log.debug("Updating mirror entry for {}: {}-->{}",
                           deviceId, localEntry, deviceEntry);
-                put(localHandle, deviceEntry);
+                put((H) localHandle, deviceEntry);
                 updateCount.incrementAndGet();
             }
         });
@@ -196,27 +210,48 @@
         }
     }
 
-    private Set<H> getHandlesForDevice(DeviceId deviceId) {
+    private Set<PiHandle> getHandlesForDevice(DeviceId deviceId) {
         return mirrorMap.keySet().stream()
                 .filter(h -> h.deviceId().equals(deviceId))
                 .collect(Collectors.toSet());
     }
 
-    @Override
-    public Map<H, E> deviceHandleMap(DeviceId deviceId) {
-        final Map<H, E> deviceMap = Maps.newHashMap();
+    private Map<PiHandle, E> deviceHandleMap(DeviceId deviceId) {
+        final Map<PiHandle, E> deviceMap = Maps.newHashMap();
         mirrorMap.entrySet().stream()
                 .filter(e -> e.getKey().deviceId().equals(deviceId))
                 .forEach(e -> deviceMap.put(e.getKey(), e.getValue().entry()));
         return deviceMap;
     }
 
+
     private void removeAll(DeviceId deviceId) {
         checkNotNull(deviceId);
-        Collection<H> handles = getHandlesForDevice(deviceId);
+        @SuppressWarnings("unchecked")
+        Collection<H> handles = (Collection<H>) getHandlesForDevice(deviceId);
         handles.forEach(this::remove);
     }
 
+    @Override
+    @SuppressWarnings("unchecked")
+    public void replayWriteResponse(P4RuntimeWriteClient.WriteResponse response) {
+        response.success().stream()
+                .filter(r -> r.entityType().equals(this.entityType) && r.isSuccess())
+                .forEach(r -> {
+                    switch (r.updateType()) {
+                        case INSERT:
+                        case MODIFY:
+                            put((H) r.handle(), (E) r.entity());
+                            break;
+                        case DELETE:
+                            remove((H) r.handle());
+                            break;
+                        default:
+                            log.error("Unknown update type {}", r.updateType());
+                    }
+                });
+    }
+
     public class InternalPipeconfWatchdogListener implements PiPipeconfWatchdogListener {
         @Override
         public void event(PiPipeconfWatchdogEvent event) {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileGroupMirror.java
index ef2d6bd..9d5d4b1 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileGroupMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileGroupMirror.java
@@ -16,10 +16,9 @@
 
 package org.onosproject.drivers.p4runtime.mirror;
 
-import org.onlab.util.KryoNamespace;
 import org.onosproject.net.pi.runtime.PiActionProfileGroup;
 import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
-import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.osgi.service.component.annotations.Component;
 
 /**
@@ -31,18 +30,7 @@
         <PiActionProfileGroupHandle, PiActionProfileGroup>
         implements P4RuntimeActionProfileGroupMirror {
 
-    private static final String DIST_MAP_NAME = "onos-p4runtime-act-prof-group-mirror";
-
-    @Override
-    String mapName() {
-        return DIST_MAP_NAME;
-    }
-
-    @Override
-    KryoNamespace storeSerializer() {
-        return KryoNamespace.newBuilder()
-                .register(KryoNamespaces.API)
-                .register(TimedEntry.class)
-                .build();
+    public DistributedP4RuntimeActionProfileGroupMirror() {
+        super(PiEntityType.ACTION_PROFILE_GROUP);
     }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileMemberMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileMemberMirror.java
index 5b2ff21..f84006b 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileMemberMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeActionProfileMemberMirror.java
@@ -16,10 +16,9 @@
 
 package org.onosproject.drivers.p4runtime.mirror;
 
-import org.onlab.util.KryoNamespace;
 import org.onosproject.net.pi.runtime.PiActionProfileMember;
 import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
-import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.osgi.service.component.annotations.Component;
 
 /**
@@ -31,18 +30,7 @@
         <PiActionProfileMemberHandle, PiActionProfileMember>
         implements P4RuntimeActionProfileMemberMirror {
 
-    private static final String DIST_MAP_NAME = "onos-p4runtime-act-prof-member-mirror";
-
-    @Override
-    String mapName() {
-        return DIST_MAP_NAME;
-    }
-
-    @Override
-    KryoNamespace storeSerializer() {
-        return KryoNamespace.newBuilder()
-                .register(KryoNamespaces.API)
-                .register(TimedEntry.class)
-                .build();
+    public DistributedP4RuntimeActionProfileMemberMirror() {
+        super(PiEntityType.ACTION_PROFILE_MEMBER);
     }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMeterMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMeterMirror.java
index bf36274..581c83a 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMeterMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMeterMirror.java
@@ -16,10 +16,9 @@
 
 package org.onosproject.drivers.p4runtime.mirror;
 
-import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterHandle;
-import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
 import org.osgi.service.component.annotations.Component;
 
 /**
@@ -28,21 +27,10 @@
 @Component(immediate = true, service = P4RuntimeMeterMirror.class)
 public final class DistributedP4RuntimeMeterMirror
         extends AbstractDistributedP4RuntimeMirror
-        <PiMeterHandle, PiMeterCellConfig>
+        <PiMeterCellHandle, PiMeterCellConfig>
         implements P4RuntimeMeterMirror {
 
-    private static final String DIST_MAP_NAME = "onos-p4runtime-meter-mirror";
-
-    @Override
-    String mapName() {
-        return DIST_MAP_NAME;
+    public DistributedP4RuntimeMeterMirror() {
+        super(PiEntityType.METER_CELL_CONFIG);
     }
-
-    @Override
-    KryoNamespace storeSerializer() {
-        return KryoNamespace.newBuilder()
-                .register(KryoNamespaces.API)
-                .register(TimedEntry.class)
-                .build();
-    }
-}
\ No newline at end of file
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java
index 83c23d8..5ccaec5 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java
@@ -16,10 +16,9 @@
 
 package org.onosproject.drivers.p4runtime.mirror;
 
-import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
-import org.onosproject.store.serializers.KryoNamespaces;
 import org.osgi.service.component.annotations.Component;
 
 /**
@@ -31,18 +30,7 @@
                         <PiMulticastGroupEntryHandle, PiMulticastGroupEntry>
         implements P4RuntimeMulticastGroupMirror {
 
-    private static final String DIST_MAP_NAME = "onos-p4runtime-mc-group-mirror";
-
-    @Override
-    String mapName() {
-        return DIST_MAP_NAME;
-    }
-
-    @Override
-    KryoNamespace storeSerializer() {
-        return KryoNamespace.newBuilder()
-                .register(KryoNamespaces.API)
-                .register(TimedEntry.class)
-                .build();
+    public DistributedP4RuntimeMulticastGroupMirror() {
+        super(PiEntityType.PRE_MULTICAST_GROUP_ENTRY);
     }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java
index 320b3da..4f5235f 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java
@@ -16,10 +16,9 @@
 
 package org.onosproject.drivers.p4runtime.mirror;
 
-import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableEntryHandle;
-import org.onosproject.store.serializers.KryoNamespaces;
 import org.osgi.service.component.annotations.Component;
 
 /**
@@ -31,18 +30,7 @@
                         <PiTableEntryHandle, PiTableEntry>
         implements P4RuntimeTableMirror {
 
-    private static final String DIST_MAP_NAME = "onos-p4runtime-table-mirror";
-
-    @Override
-    String mapName() {
-        return DIST_MAP_NAME;
-    }
-
-    @Override
-    KryoNamespace storeSerializer() {
-        return KryoNamespace.newBuilder()
-                .register(KryoNamespaces.API)
-                .register(TimedEntry.class)
-                .build();
+    public DistributedP4RuntimeTableMirror() {
+        super(PiEntityType.TABLE_ENTRY);
     }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMeterMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMeterMirror.java
index 668492a..ee88c6a 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMeterMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMeterMirror.java
@@ -18,12 +18,12 @@
 
 import com.google.common.annotations.Beta;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
 
 /**
  * Mirror of meters installed on a P4Runtime device.
  */
 @Beta
 public interface P4RuntimeMeterMirror
-        extends P4RuntimeMirror<PiMeterHandle, PiMeterCellConfig> {
+        extends P4RuntimeMirror<PiMeterCellHandle, PiMeterCellConfig> {
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
index d62bbb8..bee8c51 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
@@ -21,9 +21,9 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.pi.runtime.PiEntity;
 import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
 
 import java.util.Collection;
-import java.util.Map;
 
 /**
  * Service to keep track of the device state for a given class of PI entities.
@@ -75,15 +75,6 @@
     void remove(H handle);
 
     /**
-     * Returns a map of handles and corresponding PI entities for the given
-     * device.
-     *
-     * @param deviceId device ID
-     * @return map of handles and corresponding PI entities
-     */
-    Map<H, E> deviceHandleMap(DeviceId deviceId);
-
-    /**
      * Stores the given annotations associating it to the given handle.
      *
      * @param handle      handle
@@ -101,10 +92,18 @@
     Annotations annotations(H handle);
 
     /**
-     * Synchronizes the state of the given device ID with the given handle map.
-     *
-     * @param deviceId  device ID
-     * @param handleMap handle map
+     * Synchronizes the state of the given device ID with the given collection
+     * of PI entities.
+     * @param deviceId device ID
+     * @param entities collection of PI entities
      */
-    void sync(DeviceId deviceId, Map<H, E> handleMap);
+    void sync(DeviceId deviceId, Collection<E> entities);
+
+    /**
+     * Uses the given P4Runtime write response to update the state of this
+     * mirror.
+     *
+     * @param response P4Runtime write response
+     */
+    void replayWriteResponse(P4RuntimeWriteClient.WriteResponse response);
 }
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
index c36a77e..667897b 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
@@ -19,7 +19,7 @@
 import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionParamId;
 import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 import org.onosproject.net.pi.model.PiCounterId;
 import org.onosproject.net.pi.model.PiMatchFieldId;
 import org.onosproject.net.pi.model.PiTableId;
@@ -91,10 +91,10 @@
     public static final PiActionProfileId INGRESS_WCMP_CONTROL_WCMP_SELECTOR =
             PiActionProfileId.of("ingress.wcmp_control.wcmp_selector");
     // Packet Metadata IDs
-    public static final PiControlMetadataId PADDING =
-            PiControlMetadataId.of("_padding");
-    public static final PiControlMetadataId INGRESS_PORT =
-            PiControlMetadataId.of("ingress_port");
-    public static final PiControlMetadataId EGRESS_PORT =
-            PiControlMetadataId.of("egress_port");
+    public static final PiPacketMetadataId PADDING =
+            PiPacketMetadataId.of("_padding");
+    public static final PiPacketMetadataId INGRESS_PORT =
+            PiPacketMetadataId.of("ingress_port");
+    public static final PiPacketMetadataId EGRESS_PORT =
+            PiPacketMetadataId.of("egress_port");
 }
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
index 56a95fa..eb49819 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
@@ -22,6 +22,7 @@
 import org.onlab.packet.Ethernet;
 import org.onlab.util.ImmutableByteSequence;
 import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.Port;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.device.DeviceService;
@@ -38,7 +39,7 @@
 import org.onosproject.net.pi.model.PiTableId;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiControlMetadata;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 
 import java.nio.ByteBuffer;
@@ -173,7 +174,7 @@
     }
 
     @Override
-    public InboundPacket mapInboundPacket(PiPacketOperation packetIn)
+    public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId)
             throws PiInterpreterException {
         // Assuming that the packet is ethernet, which is fine since basic.p4
         // can deparse only ethernet packets.
@@ -186,37 +187,36 @@
         }
 
         // Returns the ingress port packet metadata.
-        Optional<PiControlMetadata> packetMetadata = packetIn.metadatas()
+        Optional<PiPacketMetadata> packetMetadata = packetIn.metadatas()
                 .stream().filter(m -> m.id().equals(INGRESS_PORT))
                 .findFirst();
 
         if (packetMetadata.isPresent()) {
             ImmutableByteSequence portByteSequence = packetMetadata.get().value();
             short s = portByteSequence.asReadOnlyBuffer().getShort();
-            ConnectPoint receivedFrom = new ConnectPoint(packetIn.deviceId(), PortNumber.portNumber(s));
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId, PortNumber.portNumber(s));
             ByteBuffer rawData = ByteBuffer.wrap(packetIn.data().asArray());
             return new DefaultInboundPacket(receivedFrom, ethPkt, rawData);
         } else {
             throw new PiInterpreterException(format(
                     "Missing metadata '%s' in packet-in received from '%s': %s",
-                    INGRESS_PORT, packetIn.deviceId(), packetIn));
+                    INGRESS_PORT, deviceId, packetIn));
         }
     }
 
     private PiPacketOperation createPiPacketOperation(ByteBuffer data, long portNumber)
             throws PiInterpreterException {
-        PiControlMetadata metadata = createPacketMetadata(portNumber);
+        PiPacketMetadata metadata = createPacketMetadata(portNumber);
         return PiPacketOperation.builder()
-                .forDevice(this.data().deviceId())
                 .withType(PACKET_OUT)
                 .withData(copyFrom(data))
                 .withMetadatas(ImmutableList.of(metadata))
                 .build();
     }
 
-    private PiControlMetadata createPacketMetadata(long portNumber) throws PiInterpreterException {
+    private PiPacketMetadata createPacketMetadata(long portNumber) throws PiInterpreterException {
         try {
-            return PiControlMetadata.builder()
+            return PiPacketMetadata.builder()
                     .withId(EGRESS_PORT)
                     .withValue(copyFrom(portNumber).fit(PORT_BITWIDTH))
                     .build();
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PortStatisticsDiscoveryImpl.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PortStatisticsDiscoveryImpl.java
index 0057293..f7cb811 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PortStatisticsDiscoveryImpl.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PortStatisticsDiscoveryImpl.java
@@ -29,6 +29,7 @@
 import org.onosproject.net.pi.model.PiCounterId;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.service.PiPipeconfService;
 import org.onosproject.p4runtime.api.P4RuntimeClient;
@@ -40,7 +41,6 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 import static org.onosproject.net.pi.model.PiCounterType.INDIRECT;
@@ -111,15 +111,14 @@
             counterCellIds.add(PiCounterCellId.ofIndirect(ingressCounterId(), p));
             counterCellIds.add(PiCounterCellId.ofIndirect(egressCounterId(), p));
         });
+        Set<PiCounterCellHandle> counterCellHandles = counterCellIds.stream()
+                .map(id -> PiCounterCellHandle.of(deviceId, id))
+                .collect(Collectors.toSet());
 
-        Collection<PiCounterCell> counterEntryResponse;
-        try {
-            counterEntryResponse = client.readCounterCells(counterCellIds, pipeconf).get();
-        } catch (InterruptedException | ExecutionException e) {
-            log.warn("Exception while reading port counters from {}: {}", deviceId, e.toString());
-            log.debug("", e);
-            return Collections.emptyList();
-        }
+        // Query the device.
+        Collection<PiCounterCell> counterEntryResponse = client.read(pipeconf)
+                .handles(counterCellHandles).submitSync()
+                .all(PiCounterCell.class);
 
         counterEntryResponse.forEach(counterCell -> {
             if (counterCell.cellId().counterType() != INDIRECT) {
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricConstants.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricConstants.java
index 9ecc65f..882a624 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricConstants.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricConstants.java
@@ -19,7 +19,7 @@
 import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionParamId;
 import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
 import org.onosproject.net.pi.model.PiCounterId;
 import org.onosproject.net.pi.model.PiMatchFieldId;
 import org.onosproject.net.pi.model.PiTableId;
@@ -254,8 +254,8 @@
     public static final PiActionProfileId FABRIC_INGRESS_NEXT_HASHED_SELECTOR =
             PiActionProfileId.of("FabricIngress.next.hashed_selector");
     // Packet Metadata IDs
-    public static final PiControlMetadataId INGRESS_PORT =
-            PiControlMetadataId.of("ingress_port");
-    public static final PiControlMetadataId EGRESS_PORT =
-            PiControlMetadataId.of("egress_port");
+    public static final PiPacketMetadataId INGRESS_PORT =
+            PiPacketMetadataId.of("ingress_port");
+    public static final PiPacketMetadataId EGRESS_PORT =
+            PiPacketMetadataId.of("egress_port");
 }
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
index 580d6f7..e523472 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
@@ -37,7 +37,7 @@
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.model.PiTableId;
 import org.onosproject.net.pi.runtime.PiAction;
-import org.onosproject.net.pi.runtime.PiControlMetadata;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 
 import java.nio.ByteBuffer;
@@ -172,19 +172,18 @@
     private PiPacketOperation createPiPacketOperation(
             DeviceId deviceId, ByteBuffer data, long portNumber)
             throws PiInterpreterException {
-        PiControlMetadata metadata = createPacketMetadata(portNumber);
+        PiPacketMetadata metadata = createPacketMetadata(portNumber);
         return PiPacketOperation.builder()
-                .forDevice(deviceId)
                 .withType(PACKET_OUT)
                 .withData(copyFrom(data))
                 .withMetadatas(ImmutableList.of(metadata))
                 .build();
     }
 
-    private PiControlMetadata createPacketMetadata(long portNumber)
+    private PiPacketMetadata createPacketMetadata(long portNumber)
             throws PiInterpreterException {
         try {
-            return PiControlMetadata.builder()
+            return PiPacketMetadata.builder()
                     .withId(FabricConstants.EGRESS_PORT)
                     .withValue(copyFrom(portNumber).fit(PORT_BITWIDTH))
                     .build();
@@ -233,10 +232,9 @@
     }
 
     @Override
-    public InboundPacket mapInboundPacket(PiPacketOperation packetIn) throws PiInterpreterException {
+    public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId) throws PiInterpreterException {
         // Assuming that the packet is ethernet, which is fine since fabric.p4
         // can deparse only ethernet packets.
-        DeviceId deviceId = packetIn.deviceId();
         Ethernet ethPkt;
         try {
             ethPkt = Ethernet.deserializer().deserialize(packetIn.data().asArray(), 0,
@@ -246,7 +244,7 @@
         }
 
         // Returns the ingress port packet metadata.
-        Optional<PiControlMetadata> packetMetadata = packetIn.metadatas()
+        Optional<PiPacketMetadata> packetMetadata = packetIn.metadatas()
                 .stream().filter(m -> m.id().equals(FabricConstants.INGRESS_PORT))
                 .findFirst();
 
diff --git a/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/AbstractGrpcClient.java b/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/AbstractGrpcClient.java
index 05e2978..0b21ca2 100644
--- a/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/AbstractGrpcClient.java
+++ b/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/AbstractGrpcClient.java
@@ -18,7 +18,6 @@
 
 import io.grpc.Context;
 import io.grpc.StatusRuntimeException;
-import org.onlab.util.SharedExecutors;
 import org.onosproject.grpc.api.GrpcClient;
 import org.onosproject.grpc.api.GrpcClientKey;
 import org.onosproject.net.DeviceId;
@@ -33,12 +32,12 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Supplier;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Abstract client for gRPC service.
- *
  */
 public abstract class AbstractGrpcClient implements GrpcClient {
 
@@ -57,6 +56,7 @@
     protected final DeviceId deviceId;
 
     protected AbstractGrpcClient(GrpcClientKey clientKey) {
+        checkNotNull(clientKey);
         this.deviceId = clientKey.deviceId();
         this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE, groupedThreads(
                 "onos-grpc-" + clientKey.serviceName() + "-client-" + deviceId.toString(), "%d"));
@@ -65,12 +65,16 @@
 
     @Override
     public CompletableFuture<Void> shutdown() {
-        return supplyWithExecutor(this::doShutdown, "shutdown",
-                SharedExecutors.getPoolThreadExecutor());
+        if (cancellableContext.isCancelled()) {
+            log.warn("Context is already cancelled, " +
+                             "ignoring request to shutdown for {}...", deviceId);
+            return CompletableFuture.completedFuture(null);
+        }
+        return CompletableFuture.supplyAsync(this::doShutdown);
     }
 
     protected Void doShutdown() {
-        log.debug("Shutting down client for {}...", deviceId);
+        log.warn("Shutting down client for {}...", deviceId);
         cancellableContext.cancel(new InterruptedException(
                 "Requested client shutdown"));
         this.executorService.shutdownNow();
@@ -84,16 +88,40 @@
     }
 
     /**
+     * Executes the given task in the cancellable context of this client.
+     *
+     * @param task task
+     * @throws IllegalStateException if context has been cancelled
+     */
+    protected void runInCancellableContext(Runnable task) {
+        if (this.cancellableContext.isCancelled()) {
+            throw new IllegalStateException(
+                    "Context is cancelled (client has been shut down)");
+        }
+        this.cancellableContext.run(task);
+    }
+
+    /**
+     * Returns the context associated with this client.
+     *
+     * @return context
+     */
+    protected Context.CancellableContext context() {
+        return cancellableContext;
+    }
+
+    /**
      * Equivalent of supplyWithExecutor using the gRPC context executor of this
      * client, such that if the context is cancelled (e.g. client shutdown) the
      * RPC is automatically cancelled.
      *
-     * @param <U> return type of supplier
-     * @param supplier the supplier to be executed
+     * @param <U>           return type of supplier
+     * @param supplier      the supplier to be executed
      * @param opDescription the description of this supplier
      * @return CompletableFuture includes the result of supplier
+     * @throws IllegalStateException if client has been shut down
      */
-    protected  <U> CompletableFuture<U> supplyInContext(
+    protected <U> CompletableFuture<U> supplyInContext(
             Supplier<U> supplier, String opDescription) {
         return supplyWithExecutor(supplier, opDescription, contextExecutor);
     }
@@ -102,37 +130,41 @@
      * Submits a task for async execution via the given executor. All tasks
      * submitted with this method will be executed sequentially.
      *
-     * @param <U> return type of supplier
-     * @param supplier the supplier to be executed
+     * @param <U>           return type of supplier
+     * @param supplier      the supplier to be executed
      * @param opDescription the description of this supplier
-     * @param executor the executor to execute this supplier
+     * @param executor      the executor to execute this supplier
      * @return CompletableFuture includes the result of supplier
+     * @throws IllegalStateException if client has been shut down
      */
     private <U> CompletableFuture<U> supplyWithExecutor(
             Supplier<U> supplier, String opDescription, Executor executor) {
+        if (this.cancellableContext.isCancelled()) {
+            throw new IllegalStateException("Client has been shut down");
+        }
         return CompletableFuture.supplyAsync(() -> {
             // TODO: explore a more relaxed locking strategy.
             try {
                 if (!requestLock.tryLock(LOCK_TIMEOUT, TimeUnit.SECONDS)) {
                     log.error("LOCK TIMEOUT! This is likely a deadlock, "
-                                    + "please debug (executing {})",
-                            opDescription);
+                                      + "please debug (executing {})",
+                              opDescription);
                     throw new IllegalThreadStateException("Lock timeout");
                 }
             } catch (InterruptedException e) {
                 log.warn("Thread interrupted while waiting for lock (executing {})",
-                        opDescription);
+                         opDescription);
                 throw new IllegalStateException(e);
             }
             try {
                 return supplier.get();
             } catch (StatusRuntimeException ex) {
                 log.warn("Unable to execute {} on {}: {}",
-                        opDescription, deviceId, ex.toString());
+                         opDescription, deviceId, ex.toString());
                 throw ex;
             } catch (Throwable ex) {
                 log.error("Exception in client of {}, executing {}",
-                        deviceId, opDescription, ex);
+                          deviceId, opDescription, ex);
                 throw ex;
             } finally {
                 requestLock.unlock();
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
index ff00a1a..31db294 100644
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,284 +16,13 @@
 
 package org.onosproject.p4runtime.api;
 
-import com.google.common.annotations.Beta;
 import org.onosproject.grpc.api.GrpcClient;
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiCounterId;
-import org.onosproject.net.pi.model.PiMeterId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiActionProfileGroup;
-import org.onosproject.net.pi.runtime.PiActionProfileMember;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import org.onosproject.net.pi.runtime.PiCounterCell;
-import org.onosproject.net.pi.runtime.PiCounterCellId;
-import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterCellId;
-import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
-import org.onosproject.net.pi.runtime.PiPacketOperation;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
 
 /**
- * Client to control a P4Runtime device.
+ * Root interface exposing all P4Runtime client capabilities.
  */
-@Beta
-public interface P4RuntimeClient extends GrpcClient {
+public interface P4RuntimeClient
+        extends GrpcClient, P4RuntimePipelineConfigClient, P4RuntimeStreamClient,
+        P4RuntimeWriteClient, P4RuntimeReadClient {
 
-    /**
-     * Type of write operation.
-     */
-    enum WriteOperationType {
-        UNSPECIFIED,
-        INSERT,
-        MODIFY,
-        DELETE
-    }
-
-    /**
-     * Starts the Stream RPC with the device.
-     *
-     * @return completable future containing true if the operation was
-     * successful, false otherwise.
-     */
-    CompletableFuture<Boolean> startStreamChannel();
-
-    /**
-     * Returns true if the stream RPC is active, false otherwise.
-     *
-     * @return boolean
-     */
-    boolean isStreamChannelOpen();
-
-    /**
-     * Sends a master arbitration update to the device with a new election ID
-     * that is guaranteed to be the highest value between all clients.
-     *
-     * @return completable future containing true if the operation was
-     * successful; false otherwise
-     */
-    CompletableFuture<Boolean> becomeMaster();
-
-    /**
-     * Returns true if this client is master for the device, false otherwise.
-     *
-     * @return boolean
-     */
-    boolean isMaster();
-
-    /**
-     * Sets the device pipeline according to the given pipeconf, and for the
-     * given byte buffer representing the target-specific data to be used in the
-     * P4Runtime's SetPipelineConfig message. This method should be called
-     * before any other method of this client.
-     *
-     * @param pipeconf   pipeconf
-     * @param deviceData target-specific data
-     * @return a completable future of a boolean, true if the operations was
-     * successful, false otherwise.
-     */
-    CompletableFuture<Boolean> setPipelineConfig(
-            PiPipeconf pipeconf, ByteBuffer deviceData);
-
-    /**
-     * Returns true if the device has the given pipeconf set, false otherwise.
-     * Equality is based on the P4Info extension of the pipeconf as well as the
-     * given device data byte buffer.
-     * <p>
-     * This method is expected to return {@code true} if invoked after calling
-     * {@link #setPipelineConfig(PiPipeconf, ByteBuffer)} with the same
-     * parameters.
-     *
-     * @param pipeconf   pipeconf
-     * @param deviceData target-specific data
-     * @return boolean
-     */
-    boolean isPipelineConfigSet(PiPipeconf pipeconf, ByteBuffer deviceData);
-
-    /**
-     * Performs the given write operation for the given table entries and
-     * pipeconf.
-     *
-     * @param entries  table entries
-     * @param opType   operation type
-     * @param pipeconf pipeconf currently deployed on the device
-     * @return true if the operation was successful, false otherwise.
-     */
-    CompletableFuture<Boolean> writeTableEntries(
-            List<PiTableEntry> entries, WriteOperationType opType,
-            PiPipeconf pipeconf);
-
-    /**
-     * Dumps all entries currently installed in the given tables, for the given
-     * pipeconf. If defaultEntries is set to true only the default action
-     * entries will be returned, otherwise non-default entries will be
-     * considered.
-     *
-     * @param tableIds       table identifiers
-     * @param defaultEntries true to read default entries, false for
-     *                       non-default
-     * @param pipeconf       pipeconf currently deployed on the device
-     * @return completable future of a list of table entries
-     */
-    CompletableFuture<List<PiTableEntry>> dumpTables(
-            Set<PiTableId> tableIds, boolean defaultEntries, PiPipeconf pipeconf);
-
-    /**
-     * Dumps entries from all tables, for the given pipeconf.
-     *
-     * @param pipeconf pipeconf currently deployed on the device
-     * @return completable future of a list of table entries
-     */
-    CompletableFuture<List<PiTableEntry>> dumpAllTables(PiPipeconf pipeconf);
-
-    /**
-     * Executes a packet-out operation for the given pipeconf.
-     *
-     * @param packet   packet-out operation to be performed by the device
-     * @param pipeconf pipeconf currently deployed on the device
-     * @return a completable future of a boolean, true if the operations was
-     * successful, false otherwise.
-     */
-    CompletableFuture<Boolean> packetOut(
-            PiPacketOperation packet, PiPipeconf pipeconf);
-
-    /**
-     * Returns the value of all counter cells for the given set of counter
-     * identifiers and pipeconf.
-     *
-     * @param counterIds counter identifiers
-     * @param pipeconf   pipeconf
-     * @return list of counter data
-     */
-    CompletableFuture<List<PiCounterCell>> readAllCounterCells(
-            Set<PiCounterId> counterIds, PiPipeconf pipeconf);
-
-    /**
-     * Returns a list of counter data corresponding to the given set of counter
-     * cell identifiers, for the given pipeconf.
-     *
-     * @param cellIds  set of counter cell identifiers
-     * @param pipeconf pipeconf
-     * @return list of counter data
-     */
-    CompletableFuture<List<PiCounterCell>> readCounterCells(
-            Set<PiCounterCellId> cellIds, PiPipeconf pipeconf);
-
-    /**
-     * Performs the given write operation for the given action profile members
-     * and pipeconf.
-     *
-     * @param members  action profile members
-     * @param opType   write operation type
-     * @param pipeconf the pipeconf currently deployed on the device
-     * @return true if the operation was successful, false otherwise
-     */
-    CompletableFuture<Boolean> writeActionProfileMembers(
-            List<PiActionProfileMember> members,
-            WriteOperationType opType, PiPipeconf pipeconf);
-
-    /**
-     * Performs the given write operation for the given action profile group and
-     * pipeconf.
-     *
-     * @param group         the action profile group
-     * @param opType        write operation type
-     * @param pipeconf      the pipeconf currently deployed on the device
-     * @return true if the operation was successful, false otherwise
-     */
-    CompletableFuture<Boolean> writeActionProfileGroup(
-            PiActionProfileGroup group,
-            WriteOperationType opType,
-            PiPipeconf pipeconf);
-
-    /**
-     * Dumps all groups for a given action profile.
-     *
-     * @param actionProfileId the action profile id
-     * @param pipeconf        the pipeconf currently deployed on the device
-     * @return completable future of a list of groups
-     */
-    CompletableFuture<List<PiActionProfileGroup>> dumpActionProfileGroups(
-            PiActionProfileId actionProfileId, PiPipeconf pipeconf);
-
-    /**
-     * Dumps all members for a given action profile.
-     *
-     * @param actionProfileId action profile ID
-     * @param pipeconf        pipeconf
-     * @return future of list of action profile member ID
-     */
-    CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
-            PiActionProfileId actionProfileId, PiPipeconf pipeconf);
-
-    /**
-     * Removes the given members from the given action profile. Returns the list
-     * of successfully removed members.
-     *
-     * @param actionProfileId action profile ID
-     * @param memberIds       member IDs
-     * @param pipeconf        pipeconf
-     * @return list of member IDs that were successfully removed from the device
-     */
-    CompletableFuture<List<PiActionProfileMemberId>> removeActionProfileMembers(
-            PiActionProfileId actionProfileId,
-            List<PiActionProfileMemberId> memberIds,
-            PiPipeconf pipeconf);
-
-    /**
-     * Returns the configuration of all meter cells for the given set of meter
-     * identifiers and pipeconf.
-     *
-     * @param meterIds meter identifiers
-     * @param pipeconf pipeconf
-     * @return list of meter configurations
-     */
-    CompletableFuture<List<PiMeterCellConfig>> readAllMeterCells(
-            Set<PiMeterId> meterIds, PiPipeconf pipeconf);
-
-    /**
-     * Returns a list of meter configurations corresponding to the given set of
-     * meter cell identifiers, for the given pipeconf.
-     *
-     * @param cellIds  set of meter cell identifiers
-     * @param pipeconf pipeconf
-     * @return list of meter configrations
-     */
-    CompletableFuture<List<PiMeterCellConfig>> readMeterCells(
-            Set<PiMeterCellId> cellIds, PiPipeconf pipeconf);
-
-    /**
-     * Performs a write operation for the given meter configurations and
-     * pipeconf.
-     *
-     * @param cellConfigs meter cell configurations
-     * @param pipeconf    pipeconf currently deployed on the device
-     * @return true if the operation was successful, false otherwise.
-     */
-    CompletableFuture<Boolean> writeMeterCells(
-            List<PiMeterCellConfig> cellConfigs, PiPipeconf pipeconf);
-
-    /**
-     * Performs the given write operation for the given PI multicast groups
-     * entries.
-     *
-     * @param entries multicast group entries
-     * @param opType  write operation type
-     * @return true if the operation was successful, false otherwise
-     */
-    CompletableFuture<Boolean> writePreMulticastGroupEntries(
-            List<PiMulticastGroupEntry> entries,
-            WriteOperationType opType);
-
-    /**
-     * Returns all multicast groups on device.
-     *
-     * @return multicast groups
-     */
-    CompletableFuture<List<PiMulticastGroupEntry>> readAllMulticastGroupEntries();
 }
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimePipelineConfigClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimePipelineConfigClient.java
new file mode 100644
index 0000000..34dac66
--- /dev/null
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimePipelineConfigClient.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.api;
+
+import com.google.common.util.concurrent.Futures;
+import org.onosproject.net.pi.model.PiPipeconf;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * P4Runtime client interface for the pipeline configuration-related RPCs.
+ */
+public interface P4RuntimePipelineConfigClient {
+
+    /**
+     * Uploads and commit a pipeline configuration to the server using the
+     * {@link PiPipeconf.ExtensionType#P4_INFO_TEXT} extension of the given
+     * pipeconf, and the given byte buffer as the target-specific data ("P4
+     * blob") to be used in the P4Runtime's {@code SetPipelineConfig} message.
+     * Returns true if the operations was successful, false otherwise.
+     *
+     * @param pipeconf   pipeconf
+     * @param deviceData target-specific data
+     * @return completable future, true if the operations was successful, false
+     * otherwise.
+     */
+    CompletableFuture<Boolean> setPipelineConfig(
+            PiPipeconf pipeconf, ByteBuffer deviceData);
+
+    /**
+     * Same as {@link #setPipelineConfig(PiPipeconf, ByteBuffer)}, but blocks
+     * execution.
+     *
+     * @param pipeconf   pipeconf
+     * @param deviceData target-specific data
+     * @return true if the operations was successful, false otherwise.
+     */
+    default boolean setPipelineConfigSync(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+        checkNotNull(pipeconf);
+        checkNotNull(deviceData);
+        return Futures.getUnchecked(setPipelineConfig(pipeconf, deviceData));
+    }
+
+    /**
+     * Returns true if the device has the given pipeconf set, false otherwise.
+     * If possible, equality should be based on {@link PiPipeconf#fingerprint()},
+     * otherwise, the implementation can request the server to send the whole
+     * P4Info and target-specific data for comparison.
+     * <p>
+     * This method is expected to return {@code true} if invoked after calling
+     * {@link #setPipelineConfig(PiPipeconf, ByteBuffer)} with the same
+     * parameters.
+     *
+     * @param pipeconf   pipeconf
+     * @param deviceData target-specific data
+     * @return completable future, true if the device has the given pipeconf
+     * set, false otherwise.
+     */
+    CompletableFuture<Boolean> isPipelineConfigSet(
+            PiPipeconf pipeconf, ByteBuffer deviceData);
+
+    /**
+     * Same as {@link #isPipelineConfigSet(PiPipeconf, ByteBuffer)} but blocks
+     * execution.
+     *
+     * @param pipeconf   pipeconf
+     * @param deviceData target-specific data
+     * @return true if the device has the given pipeconf set, false otherwise.
+     */
+    default boolean isPipelineConfigSetSync(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+        return Futures.getUnchecked(isPipelineConfigSet(pipeconf, deviceData));
+    }
+}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeReadClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeReadClient.java
new file mode 100644
index 0000000..9a47398
--- /dev/null
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeReadClient.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.api;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiCounterId;
+import org.onosproject.net.pi.model.PiMeterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * P4Runtime client interface for the Read RPC that allows reading multiple
+ * entities with one request.
+ */
+public interface P4RuntimeReadClient {
+
+    /**
+     * Returns a new {@link ReadRequest} instance that can bed used to build a
+     * batched read request, for the given pipeconf.
+     *
+     * @param pipeconf pipeconf
+     * @return new read request
+     */
+    ReadRequest read(PiPipeconf pipeconf);
+
+    /**
+     * Abstraction of a P4Runtime read request that follows the builder pattern.
+     * Multiple entities can be added to the same request before submitting it.
+     */
+    interface ReadRequest {
+
+        /**
+         * Requests to read one entity identified by the given handle.
+         *
+         * @param handle handle
+         * @return this
+         */
+        ReadRequest handle(PiHandle handle);
+
+        /**
+         * Requests to read multiple entities identified by the given handles.
+         *
+         * @param handles iterable of handles
+         * @return this
+         */
+        ReadRequest handles(Iterable<? extends PiHandle> handles);
+
+        /**
+         * Requests to read all table entries from the given table ID.
+         *
+         * @param tableId table ID
+         * @return this
+         */
+        ReadRequest tableEntries(PiTableId tableId);
+
+        /**
+         * Requests to read all table entries from the given table IDs.
+         *
+         * @param tableIds table IDs
+         * @return this
+         */
+        ReadRequest tableEntries(Iterable<PiTableId> tableIds);
+
+        /**
+         * Requests to read the default table entry from the given table.
+         *
+         * @param tableId table ID
+         * @return this
+         */
+        ReadRequest defaultTableEntry(PiTableId tableId);
+
+        /**
+         * Requests to read the default table entry from the given tables.
+         *
+         * @param tableIds table IDs
+         * @return this
+         */
+        ReadRequest defaultTableEntry(Iterable<PiTableId> tableIds);
+
+        /**
+         * Requests to read all action profile groups from the given action
+         * profile.
+         *
+         * @param actionProfileId action profile ID
+         * @return this
+         */
+        ReadRequest actionProfileGroups(PiActionProfileId actionProfileId);
+
+        /**
+         * Requests to read all action profile groups from the given action
+         * profiles.
+         *
+         * @param actionProfileIds action profile IDs
+         * @return this
+         */
+        ReadRequest actionProfileGroups(Iterable<PiActionProfileId> actionProfileIds);
+
+        /**
+         * Requests to read all action profile members from the given action
+         * profile.
+         *
+         * @param actionProfileId action profile ID
+         * @return this
+         */
+        ReadRequest actionProfileMembers(PiActionProfileId actionProfileId);
+
+        /**
+         * Requests to read all action profile members from the given action
+         * profiles.
+         *
+         * @param actionProfileIds action profile IDs
+         * @return this
+         */
+        ReadRequest actionProfileMembers(Iterable<PiActionProfileId> actionProfileIds);
+
+        /**
+         * Requests to read all counter cells from the given counter.
+         *
+         * @param counterId counter ID
+         * @return this
+         */
+        ReadRequest counterCells(PiCounterId counterId);
+
+        /**
+         * Requests to read all counter cells from the given counters.
+         *
+         * @param counterIds counter IDs
+         * @return this
+         */
+        ReadRequest counterCells(Iterable<PiCounterId> counterIds);
+
+        /**
+         * Requests to read all direct counter cells from the given table.
+         *
+         * @param tableId table ID
+         * @return this
+         */
+        ReadRequest directCounterCells(PiTableId tableId);
+
+        /**
+         * Requests to read all direct counter cells from the given tables.
+         *
+         * @param tableIds table IDs
+         * @return this
+         */
+        ReadRequest directCounterCells(Iterable<PiTableId> tableIds);
+
+        /**
+         * Requests to read all meter cell configs from the given meter ID.
+         *
+         * @param meterId meter ID
+         * @return this
+         */
+        ReadRequest meterCells(PiMeterId meterId);
+
+        /**
+         * Requests to read all meter cell configs from the given meter IDs.
+         *
+         * @param meterIds meter IDs
+         * @return this
+         */
+        ReadRequest meterCells(Iterable<PiMeterId> meterIds);
+
+        /**
+         * Requests to read all direct meter cell configs from the given table.
+         *
+         * @param tableId table ID
+         * @return this
+         */
+        ReadRequest directMeterCells(PiTableId tableId);
+
+        /**
+         * Requests to read all direct meter cell configs from the given
+         * tables.
+         *
+         * @param tableIds table IDs
+         * @return this
+         */
+        ReadRequest directMeterCells(Iterable<PiTableId> tableIds);
+
+        /**
+         * Submits the read request and returns a read response wrapped in a
+         * completable future. The future is completed once all entities have
+         * been received by the P4Runtime client.
+         *
+         * @return completable future of a read response
+         */
+        CompletableFuture<ReadResponse> submit();
+
+        /**
+         * Similar to {@link #submit()}, but blocks until the operation is
+         * completed, after which, it returns a read response.
+         *
+         * @return read response
+         */
+        ReadResponse submitSync();
+
+        //TODO: implement per-entity asynchronous reads. This would allow a user
+        // of this client to process read entities as they arrive, instead of
+        // waiting for the client to receive them all. Java 9 Reactive Streams
+        // seems a good way of doing it.
+    }
+
+    /**
+     * Response to a P4Runtime read request.
+     */
+    interface ReadResponse {
+
+        /**
+         * Returns true if the request was successful with no errors, otherwise
+         * returns false. In case of errors, further details can be obtained
+         * with {@link #explanation()} and {@link #throwable()}.
+         *
+         * @return true if the request was successful with no errors, false
+         * otherwise
+         */
+        boolean isSuccess();
+
+        /**
+         * Returns a collection of all PI entities returned by the server.
+         *
+         * @return collection of all PI entities returned by the server
+         */
+        Collection<PiEntity> all();
+
+        /**
+         * Returns a collection of all PI entities of a given class returned by
+         * the server.
+         *
+         * @param clazz PI entity class
+         * @param <E>   PI entity class
+         * @return collection of all PI entities returned by the server for the
+         * given PI entity class
+         */
+        <E extends PiEntity> Collection<E> all(Class<E> clazz);
+
+        /**
+         * If the read request was not successful, this method returns a message
+         * explaining the error occurred. Returns an empty string if such
+         * message is not available, or {@code null} if no errors occurred.
+         *
+         * @return error explanation or empty string or null
+         */
+        String explanation();
+
+        /**
+         * If the read request was not successful, this method returns the
+         * internal throwable instance associated with the error (e.g. a {@link
+         * io.grpc.StatusRuntimeException} instance). Returns null if such
+         * throwable instance is not available or if no errors occurred.
+         *
+         * @return throwable instance
+         */
+        Throwable throwable();
+    }
+}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeStreamClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeStreamClient.java
new file mode 100644
index 0000000..1317e29
--- /dev/null
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeStreamClient.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.api;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+
+/**
+ * P4Runtime client interface for the StreamChannel RPC. It allows management of
+ * the P4Runtime session (open/close, mastership arbitration) as well as sending
+ * packet-outs. All messages received from the server via the stream channel,
+ * such as master arbitration updates, or packet-ins, are handled by the
+ * P4RuntimeController. Anyone interested in those messages should register a
+ * listener with the latter.
+ */
+public interface P4RuntimeStreamClient {
+
+    /**
+     * Opens a session to the server by starting the Stream RPC and sending a
+     * mastership arbitration update message with an election ID that is
+     * expected to be unique among all available clients. If a client has been
+     * requested to become master via {@link #runForMastership()}, then this
+     * method should pick an election ID that is lower than the one currently
+     * associated with the master client.
+     * <p>
+     * If the server acknowledges the session to this client as open, the {@link
+     * P4RuntimeController} is expected to generate a {@link
+     * org.onosproject.net.device.DeviceAgentEvent} with type {@link
+     * org.onosproject.net.device.DeviceAgentEvent.Type#CHANNEL_OPEN}.
+     */
+    void openSession();
+
+    /**
+     * Returns true if the Stream RPC is active and the P4Runtime session is
+     * open, false otherwise.
+     *
+     * @return boolean
+     */
+    boolean isSessionOpen();
+
+    /**
+     * Closes the session to the server by terminating the Stream RPC.
+     */
+    void closeSession();
+
+    /**
+     * Sends a master arbitration update to the device with a new election ID
+     * that is expected to be the highest one between all clients.
+     * <p>
+     * If the server acknowledges this client as master, the {@link
+     * P4RuntimeController} is expected to generate a {@link
+     * org.onosproject.net.device.DeviceAgentEvent} with type {@link
+     * org.onosproject.net.device.DeviceAgentEvent.Type#ROLE_MASTER}.
+     */
+    void runForMastership();
+
+    /**
+     * Returns true if this client is master for the server, false otherwise.
+     *
+     * @return boolean
+     */
+    boolean isMaster();
+
+    /**
+     * Sends a packet-out for the given pipeconf.
+     *
+     * @param packet   packet-out operation to be performed by the device
+     * @param pipeconf pipeconf currently deployed on the device
+     */
+    void packetOut(PiPacketOperation packet, PiPipeconf pipeconf);
+}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeWriteClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeWriteClient.java
new file mode 100644
index 0000000..6d8dbfe
--- /dev/null
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeWriteClient.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.api;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
+import org.onosproject.net.pi.runtime.PiHandle;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * P4Runtime client interface for the Write RPC that allows inserting, modifying
+ * and deleting PI entities. Allows batching of write requests and it returns a
+ * detailed response for each PI entity in the request.
+ */
+public interface P4RuntimeWriteClient {
+
+    /**
+     * Returns a new {@link WriteRequest} instance that can be used to build a
+     * batched write request, for the given pipeconf.
+     *
+     * @param pipeconf pipeconf
+     * @return new write request
+     */
+    WriteRequest write(PiPipeconf pipeconf);
+
+    /**
+     * Signals the type of write operation for a given PI entity.
+     */
+    enum UpdateType {
+        /**
+         * Inserts an entity.
+         */
+        INSERT,
+        /**
+         * Modifies an existing entity.
+         */
+        MODIFY,
+        /**
+         * Deletes an existing entity.
+         */
+        DELETE
+    }
+
+    /**
+     * Signals if the entity was written successfully or not.
+     */
+    enum WriteResponseStatus {
+        /**
+         * The entity was written successfully, no errors occurred.
+         */
+        OK,
+        /**
+         * The server didn't return any status for the entity.
+         */
+        PENDING,
+        /**
+         * The entity was not added to the write request because it was not
+         * possible to encode/decode it.
+         */
+        CODEC_ERROR,
+        /**
+         * Server responded that it was not possible to insert the entity as
+         * another one with the same handle already exists.
+         */
+        ALREADY_EXIST,
+        /**
+         * Server responded that it was not possible to modify or delete the
+         * entity as the same cannot be found on the server.
+         */
+        NOT_FOUND,
+        /**
+         * Other error. See {@link WriteEntityResponse#explanation()} or {@link
+         * WriteEntityResponse#throwable()} for more details.
+         */
+        OTHER_ERROR,
+    }
+
+    /**
+     * Signals the atomicity mode that the server should follow when executing a
+     * write request. For more information on each atomicity mode please see the
+     * P4Runtime spec.
+     */
+    enum Atomicity {
+        /**
+         * Continue on error. Default value for all write requests.
+         */
+        CONTINUE_ON_ERROR,
+        /**
+         * Rollback on error.
+         */
+        ROLLBACK_ON_ERROR,
+        /**
+         * Dataplane atomic.
+         */
+        DATAPLANE_ATOMIC,
+    }
+
+    /**
+     * Abstraction of a P4Runtime write request that follows the builder
+     * pattern. Multiple entities can be added to the same request before
+     * submitting it. The implementation should guarantee that entities are
+     * added in the final P4Runtime protobuf message in the same order as added
+     * in this write request.
+     */
+    interface WriteRequest {
+
+        /**
+         * Sets the atomicity mode of this write request. Default value is
+         * {@link Atomicity#CONTINUE_ON_ERROR}.
+         *
+         * @param atomicity atomicity mode
+         * @return this
+         */
+        WriteRequest withAtomicity(Atomicity atomicity);
+
+        /**
+         * Requests to insert one PI entity.
+         *
+         * @param entity PI entity
+         * @return this
+         */
+        WriteRequest insert(PiEntity entity);
+
+        /**
+         * Requests to insert multiple PI entities.
+         *
+         * @param entities iterable of PI entities
+         * @return this
+         */
+        WriteRequest insert(Iterable<? extends PiEntity> entities);
+
+        /**
+         * Requests to modify one PI entity.
+         *
+         * @param entity PI entity
+         * @return this
+         */
+        WriteRequest modify(PiEntity entity);
+
+        /**
+         * Requests to modify multiple PI entities.
+         *
+         * @param entities iterable of PI entities
+         * @return this
+         */
+        WriteRequest modify(Iterable<? extends PiEntity> entities);
+
+        /**
+         * Requests to delete one PI entity identified by the given handle.
+         *
+         * @param handle PI handle
+         * @return this
+         */
+        WriteRequest delete(PiHandle handle);
+
+        /**
+         * Requests to delete multiple PI entities identified by the given
+         * handles.
+         *
+         * @param handles iterable of handles
+         * @return this
+         */
+        WriteRequest delete(Iterable<? extends PiHandle> handles);
+
+        /**
+         * Requests to write the given PI entity with the given update type. If
+         * {@code updateType} is {@link UpdateType#DELETE}, then only the handle
+         * will be considered by the request.
+         *
+         * @param entity     PI entity
+         * @param updateType update type
+         * @return this
+         */
+        WriteRequest entity(PiEntity entity, UpdateType updateType);
+
+        /**
+         * Requests to write the given PI entities with the given update type.
+         * If {@code updateType} is {@link UpdateType#DELETE}, then only the
+         * handles will be considered by the request.
+         *
+         * @param entities   iterable of PI entity
+         * @param updateType update type
+         * @return this
+         */
+        WriteRequest entities(Iterable<? extends PiEntity> entities, UpdateType updateType);
+
+        /**
+         * Submits this write request to the server and returns a completable
+         * future holding the response. The future is completed only after the
+         * server signals that all entities are written.
+         *
+         * @return completable future of the write response
+         */
+        CompletableFuture<WriteResponse> submit();
+
+        /**
+         * Similar to {@link #submit()}, but blocks until the operation is
+         * completed, after which, it returns a read response.
+         *
+         * @return read response
+         */
+        P4RuntimeWriteClient.WriteResponse submitSync();
+    }
+
+    /**
+     * Abstraction of a response obtained from a P4Runtime server after a write
+     * request is submitted. It allows returning a detailed response ({@link
+     * WriteEntityResponse}) for each PI entity in the original request. Entity
+     * responses are guaranteed to be returned in the same order as the
+     * corresponding PI entity in the request.
+     */
+    interface WriteResponse {
+
+        /**
+         * Returns true if all entities in the request were successfully
+         * written. In other words, if no errors occurred. False otherwise.
+         *
+         * @return true if all entities were written successfully, false
+         * otherwise
+         */
+        boolean isSuccess();
+
+        /**
+         * Returns a detailed response for each PI entity in the request. The
+         * implementation of this method should guarantee that the returned
+         * collection has size equal to the number of PI entities in the
+         * original write request.
+         *
+         * @return collection of {@link WriteEntityResponse}
+         */
+        Collection<WriteEntityResponse> all();
+
+        /**
+         * Returns a detailed response for each PI entity that was successfully
+         * written. If {@link #isSuccess()} is {@code true}, then this method is
+         * expected to return the same values as {@link #all()}.
+         *
+         * @return collection of {@link WriteEntityResponse}
+         */
+        Collection<WriteEntityResponse> success();
+
+        /**
+         * Returns a detailed response for each PI entity for which the server
+         * returned an error. If {@link #isSuccess()} is {@code true}, then this
+         * method is expected to return an empty collection.
+         *
+         * @return collection of {@link WriteEntityResponse}
+         */
+        Collection<WriteEntityResponse> failed();
+
+        /**
+         * Returns a detailed response for each PI entity for which the server
+         * returned the given status.
+         *
+         * @param status status
+         * @return collection of {@link WriteEntityResponse}
+         */
+        Collection<WriteEntityResponse> status(WriteResponseStatus status);
+    }
+
+    /**
+     * Represents the response of a write request for a specific PI entity.
+     */
+    interface WriteEntityResponse {
+
+        /**
+         * Returns the handle associated with the PI entity.
+         *
+         * @return handle
+         */
+        PiHandle handle();
+
+        /**
+         * Returns the original PI entity as provided in the write request.
+         * Returns {@code null} if the update type was {@link
+         * UpdateType#DELETE}, in which case only the handle was used in the
+         * request.
+         *
+         * @return PI entity or null
+         */
+        PiEntity entity();
+
+        /**
+         * Returns the type of write request performed for this entity.
+         *
+         * @return update type
+         */
+        UpdateType updateType();
+
+        /**
+         * Returns the type of this entity.
+         *
+         * @return PI entity type
+         */
+        PiEntityType entityType();
+
+        /**
+         * Returns true if this PI entity was written successfully, false
+         * otherwise.
+         *
+         * @return true if this PI entity was written successfully, false
+         * otherwise
+         */
+        boolean isSuccess();
+
+        /**
+         * Returns the status for this PI entity. If {@link #isSuccess()}
+         * returns {@code true}, then this method is expected to return {@link
+         * WriteResponseStatus#OK}. If {@link WriteResponseStatus#OTHER_ERROR}
+         * is returned, further details might be provided in {@link
+         * #explanation()} and {@link #throwable()}.
+         *
+         * @return status
+         */
+        WriteResponseStatus status();
+
+        /**
+         * If the PI entity was NOT written successfully, this method returns a
+         * message explaining the error occurred. Returns an empty string if
+         * such message is not available, or {@code null} if no errors
+         * occurred.
+         *
+         * @return error explanation or empty string or null
+         */
+        String explanation();
+
+        /**
+         * If the PI entity was NOT written successfully, this method returns
+         * the internal throwable instance associated with the error (e.g. a
+         * {@link io.grpc.StatusRuntimeException} instance). Returns null if
+         * such throwable instance is not available or if no errors occurred.
+         *
+         * @return throwable instance associated with this PI entity
+         */
+        Throwable throwable();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
deleted file mode 100644
index a76ab7c..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright 2019-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import com.google.protobuf.Message;
-import com.google.protobuf.TextFormat;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiEntity;
-import org.slf4j.Logger;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.lang.String.format;
-import static org.slf4j.LoggerFactory.getLogger;
-
-/**
- * Abstract implementation of a codec that translates PI entities into P4Runtime
- * protobuf messages and vice versa.
- *
- * @param <P> PI entity class
- * @param <M> P4Runtime protobuf message class
- */
-abstract class AbstractP4RuntimeCodec<P extends PiEntity, M extends Message> {
-
-    protected final Logger log = getLogger(this.getClass());
-
-    protected abstract M encode(P piEntity, PiPipeconf pipeconf,
-                                P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException;
-
-    protected abstract P decode(M message, PiPipeconf pipeconf,
-                                P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException;
-
-    /**
-     * Returns a P4Runtime protobuf message that is equivalent to the given PI
-     * entity for the given pipeconf.
-     *
-     * @param piEntity PI entity instance
-     * @param pipeconf pipeconf
-     * @return P4Runtime protobuf message
-     * @throws CodecException if the given PI entity cannot be encoded (see
-     *                        exception message)
-     */
-    public M encode(P piEntity, PiPipeconf pipeconf)
-            throws CodecException {
-        try {
-            return encode(piEntity, pipeconf, browserOrFail(pipeconf));
-        } catch (P4InfoBrowser.NotFoundException e) {
-            throw new CodecException(e.getMessage());
-        }
-    }
-
-    /**
-     * Returns a PI entity instance that is equivalent to the P4Runtime protobuf
-     * message for the given pipeconf.
-     *
-     * @param message  P4Runtime protobuf message
-     * @param pipeconf pipeconf pipeconf
-     * @return PI entity instance
-     * @throws CodecException if the given protobuf message cannot be decoded
-     *                        (see exception message)
-     */
-    public P decode(M message, PiPipeconf pipeconf)
-            throws CodecException {
-        try {
-            return decode(message, pipeconf, browserOrFail(pipeconf));
-        } catch (P4InfoBrowser.NotFoundException e) {
-            throw new CodecException(e.getMessage());
-        }
-    }
-
-    /**
-     * Same as {@link #encode(PiEntity, PiPipeconf)} but returns null in case of
-     * exceptions, while the error message is logged.
-     *
-     * @param piEntity PI entity instance
-     * @param pipeconf pipeconf
-     * @return P4Runtime protobuf message
-     */
-    public M encodeOrNull(P piEntity, PiPipeconf pipeconf) {
-        try {
-            return encode(piEntity, pipeconf);
-        } catch (CodecException e) {
-            log.error("Unable to encode {}: {} [{}]",
-                      piEntity.getClass().getSimpleName(),
-                      e.getMessage(), piEntity.toString());
-            return null;
-        }
-    }
-
-    /**
-     * Same as {@link #decode(Message, PiPipeconf)} but returns null in case of
-     * exceptions, while the error message is logged.
-     *
-     * @param message  P4Runtime protobuf message
-     * @param pipeconf pipeconf pipeconf
-     * @return PI entity instance
-     */
-    public P decodeOrNull(M message, PiPipeconf pipeconf) {
-        try {
-            return decode(message, pipeconf);
-        } catch (CodecException e) {
-            log.error("Unable to decode {}: {} [{}]",
-                      message.getClass().getSimpleName(),
-                      e.getMessage(), TextFormat.shortDebugString(message));
-            return null;
-        }
-    }
-
-    /**
-     * Encodes the given list of PI entities, skipping those that cannot be
-     * encoded, in which case an error message is logged. For this reason, the
-     * returned list might have different size than the returned one.
-     *
-     * @param piEntities list of PI entities
-     * @param pipeconf   pipeconf
-     * @return list of P4Runtime protobuf messages
-     */
-    public List<M> encodeAll(List<P> piEntities, PiPipeconf pipeconf) {
-        checkNotNull(piEntities);
-        return piEntities.stream()
-                .map(p -> encodeOrNull(p, pipeconf))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Decodes the given list of P4Runtime protobuf messages, skipping those
-     * that cannot be decoded, on which case an error message is logged. For
-     * this reason, the returned list might have different size than the
-     * returned one.
-     *
-     * @param messages list of protobuf messages
-     * @param pipeconf pipeconf
-     * @return list of PI entities
-     */
-    public List<P> decodeAll(List<M> messages, PiPipeconf pipeconf) {
-        checkNotNull(messages);
-        return messages.stream()
-                .map(m -> decodeOrNull(m, pipeconf))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Same as {@link #encodeAll(List, PiPipeconf)} but throws an exception if
-     * one or more of the given PI entities cannot be encoded. The returned list
-     * is guaranteed to have same size and order as the given one.
-     *
-     * @param piEntities list of PI entities
-     * @param pipeconf   pipeconf
-     * @return list of protobuf messages
-     * @throws CodecException if one or more of the given PI entities cannot be
-     *                        encoded
-     */
-    public List<M> encodeAllOrFail(List<P> piEntities, PiPipeconf pipeconf)
-            throws CodecException {
-        final List<M> messages = encodeAll(piEntities, pipeconf);
-        if (piEntities.size() != messages.size()) {
-            throw new CodecException(format(
-                    "Unable to encode %d entities of %d given " +
-                            "(see previous logs for details)",
-                    piEntities.size() - messages.size(), piEntities.size()));
-        }
-        return messages;
-    }
-
-    /**
-     * Same as {@link #decodeAll(List, PiPipeconf)} but throws an exception if
-     * one or more of the given protobuf messages cannot be decoded. The
-     * returned list is guaranteed to have same size and order as the given
-     * one.
-     *
-     * @param messages list of protobuf messages
-     * @param pipeconf pipeconf
-     * @return list of PI entities
-     * @throws CodecException if one or more of the given protobuf messages
-     *                        cannot be decoded
-     */
-    public List<P> decodeAllOrFail(List<M> messages, PiPipeconf pipeconf)
-            throws CodecException {
-        final List<P> piEntities = decodeAll(messages, pipeconf);
-        if (messages.size() != piEntities.size()) {
-            throw new CodecException(format(
-                    "Unable to decode %d messages of %d given " +
-                            "(see previous logs for details)",
-                    messages.size() - piEntities.size(), messages.size()));
-        }
-        return piEntities;
-    }
-
-    private P4InfoBrowser browserOrFail(PiPipeconf pipeconf) throws CodecException {
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new CodecException(format(
-                    "Unable to get P4InfoBrowser for pipeconf %s", pipeconf.id()));
-        }
-        return browser;
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
deleted file mode 100644
index 684ef04..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2019-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileGroup;
-import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
-
-/**
- * Codec for P4Runtime ActionProfileGroup.
- */
-final class ActionProfileGroupCodec
-        extends AbstractP4RuntimeCodec<PiActionProfileGroup, ActionProfileGroup> {
-
-    @Override
-    public ActionProfileGroup encode(
-            PiActionProfileGroup piGroup, PiPipeconf pipeconf, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException {
-
-        final int p4ActionProfileId = browser.actionProfiles()
-                .getByName(piGroup.actionProfile().id())
-                .getPreamble().getId();
-        final ActionProfileGroup.Builder msgBuilder = ActionProfileGroup.newBuilder()
-                .setGroupId(piGroup.id().id())
-                .setActionProfileId(p4ActionProfileId)
-                .setMaxSize(piGroup.maxSize());
-        piGroup.members().forEach(m -> {
-            // TODO: currently we don't set "watch" field
-            ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
-                    .setMemberId(m.id().id())
-                    .setWeight(m.weight())
-                    .build();
-            msgBuilder.addMembers(member);
-        });
-
-        return msgBuilder.build();
-    }
-
-    @Override
-    public PiActionProfileGroup decode(
-            ActionProfileGroup msg, PiPipeconf pipeconf, P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        final PiActionProfileGroup.Builder piGroupBuilder = PiActionProfileGroup.builder()
-                .withActionProfileId(PiActionProfileId.of(
-                        browser.actionProfiles()
-                                .getById(msg.getActionProfileId())
-                                .getPreamble().getName()))
-                .withId(PiActionProfileGroupId.of(msg.getGroupId()))
-                .withMaxSize(msg.getMaxSize());
-
-        msg.getMembersList().forEach(m -> {
-            int weight = m.getWeight();
-            if (weight < 1) {
-                // FIXME: currently PI has a bug which will always return weight 0
-                // ONOS won't accept group buckets with weight 0
-                log.warn("Decoding ActionProfileGroup with 'weight' " +
-                                 "field {}, will set to 1", weight);
-                weight = 1;
-            }
-            piGroupBuilder.addMember(PiActionProfileMemberId.of(
-                    m.getMemberId()), weight);
-        });
-        return piGroupBuilder.build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
deleted file mode 100644
index 9567b0a..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2019-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileMember;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decodeActionMsg;
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encodePiAction;
-/**
- * Codec for P4Runtime ActionProfileMember.
- */
-final class ActionProfileMemberCodec
-        extends AbstractP4RuntimeCodec<PiActionProfileMember, ActionProfileMember> {
-
-    @Override
-    public ActionProfileMember encode(PiActionProfileMember piEntity,
-                                      PiPipeconf pipeconf,
-                                      P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-        final ActionProfileMember.Builder actionProfileMemberBuilder =
-                ActionProfileMember.newBuilder();
-        // Member ID
-        actionProfileMemberBuilder.setMemberId(piEntity.id().id());
-        // Action profile ID
-        P4InfoOuterClass.ActionProfile actionProfile =
-                browser.actionProfiles().getByName(piEntity.actionProfile().id());
-        final int actionProfileId = actionProfile.getPreamble().getId();
-        actionProfileMemberBuilder.setActionProfileId(actionProfileId);
-        // Action
-        final P4RuntimeOuterClass.Action action = encodePiAction(piEntity.action(), browser);
-        actionProfileMemberBuilder.setAction(action);
-        return actionProfileMemberBuilder.build();
-    }
-
-    @Override
-    public PiActionProfileMember decode(ActionProfileMember message,
-                                        PiPipeconf pipeconf,
-                                        P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-        final PiActionProfileId actionProfileId = PiActionProfileId.of(
-                browser.actionProfiles()
-                        .getById(message.getActionProfileId())
-                        .getPreamble()
-                        .getName());
-        return PiActionProfileMember.builder()
-                .forActionProfile(actionProfileId)
-                .withId(PiActionProfileMemberId.of(message.getMemberId()))
-                .withAction(decodeActionMsg(message.getAction(), browser))
-                .build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
deleted file mode 100644
index 3765d24..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.pi.model.PiCounterId;
-import org.onosproject.net.pi.model.PiCounterType;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiCounterCell;
-import org.onosproject.net.pi.runtime.PiCounterCellId;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.slf4j.Logger;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.CounterData;
-import p4.v1.P4RuntimeOuterClass.CounterEntry;
-import p4.v1.P4RuntimeOuterClass.DirectCounterEntry;
-import p4.v1.P4RuntimeOuterClass.Entity;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static java.lang.String.format;
-import static org.onosproject.p4runtime.ctl.P4RuntimeUtils.indexMsg;
-import static org.slf4j.LoggerFactory.getLogger;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_COUNTER_ENTRY;
-
-/**
- * Encoder/decoder of PI counter IDs to counter entry protobuf messages, and
- * vice versa.
- */
-final class CounterEntryCodec {
-
-    private static final Logger log = getLogger(CounterEntryCodec.class);
-
-    private CounterEntryCodec() {
-        // Hides constructor.
-    }
-
-    /**
-     * Returns a collection of P4Runtime entity protobuf messages describing
-     * both counter or direct counter entries, encoded from the given collection
-     * of PI counter cell identifiers, for the given pipeconf. If a PI counter
-     * cell identifier cannot be encoded, it is skipped, hence the returned
-     * collection might have different size than the input one.
-     *
-     * @param cellIds  counter cell identifiers
-     * @param pipeconf pipeconf
-     * @return collection of entity messages describing both counter or direct
-     * counter entries
-     */
-    static List<Entity> encodePiCounterCellIds(List<PiCounterCellId> cellIds,
-                                                     PiPipeconf pipeconf) {
-
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return cellIds
-                .stream()
-                .map(cellId -> {
-                    try {
-                        return encodePiCounterCellId(cellId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
-                        log.warn("Unable to encode PI counter cell id: {}", e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Returns a collection of P4Runtime entity protobuf messages to be used in
-     * requests to read all cells from the given counter identifiers. Works for
-     * both indirect or direct counters. If a PI counter identifier cannot be
-     * encoded, it is skipped, hence the returned collection might have
-     * different size than the input one.
-     *
-     * @param counterIds counter identifiers
-     * @param pipeconf   pipeconf
-     * @return collection of entity messages
-     */
-    static List<Entity> readAllCellsEntities(List<PiCounterId> counterIds,
-                                                   PiPipeconf pipeconf) {
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return counterIds
-                .stream()
-                .map(counterId -> {
-                    try {
-                        return readAllCellsEntity(counterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
-                        log.warn("Unable to encode counter ID to read-all-cells entity: {}",
-                                 e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Returns a collection of PI counter cell data, decoded from the given
-     * P4Runtime entity protobuf messages describing both counter or direct
-     * counter entries, and pipeconf. If an entity message cannot be encoded, it
-     * is skipped, hence the returned collection might have different size than
-     * the input one.
-     *
-     * @param entities P4Runtime entity messages
-     * @param pipeconf pipeconf
-     * @return collection of PI counter cell data
-     */
-    static List<PiCounterCell> decodeCounterEntities(List<Entity> entities,
-                                                     PiPipeconf pipeconf) {
-
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return entities
-                .stream()
-                .filter(entity -> entity.getEntityCase() == COUNTER_ENTRY ||
-                        entity.getEntityCase() == DIRECT_COUNTER_ENTRY)
-                .map(entity -> {
-                    try {
-                        return decodeCounterEntity(entity, pipeconf, browser);
-                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
-                        log.warn("Unable to decode counter entity message: {}",
-                                 e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    private static Entity encodePiCounterCellId(PiCounterCellId cellId,
-                                                PiPipeconf pipeconf,
-                                                P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        int counterId;
-        Entity entity;
-        // Encode PI cell ID into entity message and add to read request.
-        switch (cellId.counterType()) {
-            case INDIRECT:
-                counterId = browser.counters()
-                        .getByName(cellId.counterId().id())
-                        .getPreamble()
-                        .getId();
-                entity = Entity.newBuilder()
-                        .setCounterEntry(
-                                CounterEntry.newBuilder()
-                                        .setCounterId(counterId)
-                                        .setIndex(indexMsg(cellId.index()))
-                                        .build())
-                        .build();
-                break;
-            case DIRECT:
-                DirectCounterEntry.Builder entryBuilder = DirectCounterEntry.newBuilder();
-                entryBuilder.setTableEntry(
-                        TableEntryEncoder.encode(cellId.tableEntry(), pipeconf));
-                entity = Entity.newBuilder()
-                        .setDirectCounterEntry(entryBuilder.build())
-                        .build();
-                break;
-            default:
-                throw new CodecException(format(
-                        "Unrecognized PI counter cell ID type '%s'",
-                        cellId.counterType()));
-        }
-
-        return entity;
-    }
-
-    private static Entity readAllCellsEntity(PiCounterId counterId,
-                                             PiPipeconf pipeconf,
-                                             P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        if (!pipeconf.pipelineModel().counter(counterId).isPresent()) {
-            throw new CodecException(format(
-                    "not such counter '%s' in pipeline model", counterId));
-        }
-        final PiCounterType counterType = pipeconf.pipelineModel()
-                .counter(counterId).get().counterType();
-
-        switch (counterType) {
-            case INDIRECT:
-                final int p4InfoCounterId = browser.counters()
-                        .getByName(counterId.id())
-                        .getPreamble().getId();
-                return Entity.newBuilder().setCounterEntry(
-                        P4RuntimeOuterClass.CounterEntry.newBuilder()
-                                // Index unset to read all cells
-                                .setCounterId(p4InfoCounterId)
-                                .build())
-                        .build();
-            case DIRECT:
-                final PiTableId tableId = pipeconf.pipelineModel()
-                        .counter(counterId).get().table();
-                if (tableId == null) {
-                    throw new CodecException(format(
-                            "null table for direct counter '%s'", counterId));
-                }
-                final int p4TableId = browser.tables().getByName(tableId.id())
-                        .getPreamble().getId();
-                return Entity.newBuilder().setDirectCounterEntry(
-                        P4RuntimeOuterClass.DirectCounterEntry.newBuilder()
-                                .setTableEntry(
-                                        // Match unset to read all cells
-                                        P4RuntimeOuterClass.TableEntry.newBuilder()
-                                                .setTableId(p4TableId)
-                                                .build())
-                                .build())
-                        .build();
-            default:
-                throw new CodecException(format(
-                        "unrecognized PI counter type '%s'", counterType));
-        }
-    }
-
-    private static PiCounterCell decodeCounterEntity(Entity entity,
-                                                     PiPipeconf pipeconf,
-                                                     P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        CounterData counterData;
-        PiCounterCellId piCellId;
-
-        if (entity.getEntityCase() == COUNTER_ENTRY) {
-            String counterName = browser.counters()
-                    .getById(entity.getCounterEntry().getCounterId())
-                    .getPreamble()
-                    .getName();
-            piCellId = PiCounterCellId.ofIndirect(
-                    PiCounterId.of(counterName),
-                    entity.getCounterEntry().getIndex().getIndex());
-            counterData = entity.getCounterEntry().getData();
-        } else if (entity.getEntityCase() == DIRECT_COUNTER_ENTRY) {
-            PiTableEntry piTableEntry = TableEntryEncoder.decode(
-                    entity.getDirectCounterEntry().getTableEntry(), pipeconf);
-            piCellId = PiCounterCellId.ofDirect(piTableEntry);
-            counterData = entity.getDirectCounterEntry().getData();
-        } else {
-            throw new CodecException(format(
-                    "Unrecognized entity type '%s' in P4Runtime message",
-                    entity.getEntityCase().name()));
-        }
-
-        return new PiCounterCell(piCellId,
-                                 counterData.getPacketCount(),
-                                 counterData.getByteCount());
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
deleted file mode 100644
index 44f27d1..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.pi.model.PiMeterId;
-import org.onosproject.net.pi.model.PiMeterType;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiMeterBand;
-import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterCellId;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.slf4j.Logger;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.DirectMeterEntry;
-import p4.v1.P4RuntimeOuterClass.Entity;
-import p4.v1.P4RuntimeOuterClass.MeterConfig;
-import p4.v1.P4RuntimeOuterClass.MeterEntry;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static java.lang.String.format;
-import static org.onosproject.p4runtime.ctl.P4RuntimeUtils.indexMsg;
-import static org.slf4j.LoggerFactory.getLogger;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_METER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.METER_ENTRY;
-
-/**
- * Encoder/decoder of PI meter cell configurations to meter entry protobuf
- * messages, and vice versa.
- */
-final class MeterEntryCodec {
-
-    private static final Logger log = getLogger(MeterEntryCodec.class);
-
-    private MeterEntryCodec() {
-        // Hides constructor.
-    }
-
-    /**
-     * Returns a collection of P4Runtime entity protobuf messages describing
-     * both meter or direct meter entries, encoded from the given collection of
-     * PI meter cell configurations, for the given pipeconf. If a PI meter cell
-     * configurations cannot be encoded, it is skipped, hence the returned
-     * collection might have different size than the input one.
-     *
-     * @param cellConfigs meter cell configurations
-     * @param pipeconf    pipeconf
-     * @return collection of entity messages describing both meter or direct
-     * meter entries
-     */
-    static List<Entity> encodePiMeterCellConfigs(List<PiMeterCellConfig> cellConfigs,
-                                                       PiPipeconf pipeconf) {
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return cellConfigs
-                .stream()
-                .map(cellConfig -> {
-                    try {
-                        return encodePiMeterCellConfig(cellConfig, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
-                        log.warn("Unable to encode PI meter cell id: {}", e.getMessage());
-                        log.debug("exception", e);
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Returns a collection of P4Runtime entity protobuf messages to be used in
-     * requests to read all cells from the given meter identifiers. Works for
-     * both indirect or direct meters. If a PI meter identifier cannot be
-     * encoded, it is skipped, hence the returned collection might have
-     * different size than the input one.
-     *
-     * @param meterIds meter identifiers
-     * @param pipeconf pipeconf
-     * @return collection of entity messages
-     */
-    static List<Entity> readAllCellsEntities(List<PiMeterId> meterIds,
-                                                   PiPipeconf pipeconf) {
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return meterIds
-                .stream()
-                .map(meterId -> {
-                    try {
-                        return readAllCellsEntity(meterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
-                        log.warn("Unable to encode meter ID to read-all-cells entity: {}",
-                                 e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Returns a collection of PI meter cell configurations, decoded from the
-     * given P4Runtime entity protobuf messages describing both meter or direct
-     * meter entries, and pipeconf. If an entity message cannot be encoded, it
-     * is skipped, hence the returned collection might have different size than
-     * the input one.
-     *
-     * @param entities P4Runtime entity messages
-     * @param pipeconf pipeconf
-     * @return collection of PI meter cell data
-     */
-    static List<PiMeterCellConfig> decodeMeterEntities(List<Entity> entities,
-                                                       PiPipeconf pipeconf) {
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}", pipeconf.id());
-            return Collections.emptyList();
-        }
-
-        return entities
-                .stream()
-                .filter(entity -> entity.getEntityCase() == METER_ENTRY ||
-                        entity.getEntityCase() == DIRECT_METER_ENTRY)
-                .map(entity -> {
-                    try {
-                        return decodeMeterEntity(entity, pipeconf, browser);
-                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
-                        log.warn("Unable to decode meter entity message: {}", e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    private static Entity encodePiMeterCellConfig(PiMeterCellConfig config,
-                                                  PiPipeconf pipeconf,
-                                                  P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        int meterId;
-        Entity entity;
-        MeterConfig meterConfig;
-
-        PiMeterBand[] bands = config.meterBands()
-                .toArray(new PiMeterBand[config.meterBands().size()]);
-        if (bands.length == 2) {
-            long cir, cburst, pir, pburst;
-            // The band with bigger burst is peak if rate of them is equal.
-            if (bands[0].rate() > bands[1].rate() ||
-                    (bands[0].rate() == bands[1].rate() &&
-                            bands[0].burst() >= bands[1].burst())) {
-                cir = bands[1].rate();
-                cburst = bands[1].burst();
-                pir = bands[0].rate();
-                pburst = bands[0].burst();
-            } else {
-                cir = bands[0].rate();
-                cburst = bands[0].burst();
-                pir = bands[1].rate();
-                pburst = bands[1].burst();
-            }
-            meterConfig = MeterConfig.newBuilder()
-                    .setCir(cir)
-                    .setCburst(cburst)
-                    .setPir(pir)
-                    .setPburst(pburst)
-                    .build();
-        } else if (bands.length == 0) {
-            // When reading meter cells.
-            meterConfig = null;
-        } else {
-            throw new CodecException("number of meter bands should be either 2 or 0");
-        }
-
-        switch (config.cellId().meterType()) {
-            case INDIRECT:
-                meterId = browser.meters()
-                        .getByName(config.cellId().meterId().id())
-                        .getPreamble().getId();
-                MeterEntry.Builder indEntryBuilder = MeterEntry.newBuilder()
-                        .setMeterId(meterId)
-                        .setIndex(indexMsg(config.cellId().index()));
-                if (meterConfig != null) {
-                    indEntryBuilder.setConfig(meterConfig);
-                }
-                entity = Entity.newBuilder()
-                        .setMeterEntry(indEntryBuilder.build()).build();
-                break;
-            case DIRECT:
-                DirectMeterEntry.Builder dirEntryBuilder = DirectMeterEntry.newBuilder()
-                        .setTableEntry(TableEntryEncoder.encode(
-                                config.cellId().tableEntry(), pipeconf));
-                if (meterConfig != null) {
-                    dirEntryBuilder.setConfig(meterConfig);
-                }
-                entity = Entity.newBuilder()
-                        .setDirectMeterEntry(dirEntryBuilder.build()).build();
-                break;
-            default:
-                throw new CodecException(format("unrecognized PI meter type '%s'",
-                                                config.cellId().meterType()));
-        }
-
-        return entity;
-    }
-
-    private static Entity readAllCellsEntity(PiMeterId meterId,
-                                             PiPipeconf pipeconf,
-                                             P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        if (!pipeconf.pipelineModel().meter(meterId).isPresent()) {
-            throw new CodecException(format(
-                    "not such meter '%s' in pipeline model", meterId));
-        }
-        final PiMeterType meterType = pipeconf.pipelineModel()
-                .meter(meterId).get().meterType();
-
-        switch (meterType) {
-            case INDIRECT:
-                final int p4InfoMeterId = browser.meters()
-                        .getByName(meterId.id())
-                        .getPreamble().getId();
-                return Entity.newBuilder().setMeterEntry(
-                        P4RuntimeOuterClass.MeterEntry.newBuilder()
-                                // Index unset to read all cells
-                                .setMeterId(p4InfoMeterId)
-                                .build())
-                        .build();
-            case DIRECT:
-                final PiTableId tableId = pipeconf.pipelineModel()
-                        .meter(meterId).get().table();
-                if (tableId == null) {
-                    throw new CodecException(format(
-                            "null table for direct meter '%s'", meterId));
-                }
-                final int p4TableId = browser.tables().getByName(tableId.id())
-                        .getPreamble().getId();
-                return Entity.newBuilder().setDirectMeterEntry(
-                        P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
-                                .setTableEntry(
-                                        // Match unset to read all cells
-                                        P4RuntimeOuterClass.TableEntry.newBuilder()
-                                                .setTableId(p4TableId)
-                                                .build())
-                                .build())
-                        .build();
-            default:
-                throw new CodecException(format(
-                        "unrecognized PI meter type '%s'", meterType));
-        }
-    }
-
-    private static PiMeterCellConfig decodeMeterEntity(Entity entity,
-                                                       PiPipeconf pipeconf,
-                                                       P4InfoBrowser browser)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        MeterConfig meterConfig;
-        PiMeterCellId piCellId;
-
-        if (entity.getEntityCase() == METER_ENTRY) {
-            String meterName = browser.meters()
-                    .getById(entity.getMeterEntry().getMeterId())
-                    .getPreamble()
-                    .getName();
-            piCellId = PiMeterCellId.ofIndirect(
-                    PiMeterId.of(meterName),
-                    entity.getMeterEntry().getIndex().getIndex());
-            meterConfig = entity.getMeterEntry().getConfig();
-        } else if (entity.getEntityCase() == DIRECT_METER_ENTRY) {
-            PiTableEntry piTableEntry = TableEntryEncoder.decode(
-                    entity.getDirectMeterEntry().getTableEntry(),
-                    pipeconf);
-            piCellId = PiMeterCellId.ofDirect(piTableEntry);
-            meterConfig = entity.getDirectMeterEntry().getConfig();
-        } else {
-            throw new CodecException(format(
-                    "unrecognized entity type '%s' in P4Runtime message",
-                    entity.getEntityCase().name()));
-        }
-
-        return PiMeterCellConfig.builder()
-                .withMeterCellId(piCellId)
-                .withMeterBand(new PiMeterBand(meterConfig.getCir(),
-                                               meterConfig.getCburst()))
-                .withMeterBand(new PiMeterBand(meterConfig.getPir(),
-                                               meterConfig.getPburst()))
-                .build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
deleted file mode 100644
index 5f55c1f..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.PortNumber;
-import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
-import org.onosproject.net.pi.runtime.PiPreReplica;
-import p4.v1.P4RuntimeOuterClass.MulticastGroupEntry;
-import p4.v1.P4RuntimeOuterClass.Replica;
-
-import static java.lang.String.format;
-
-/**
- * A coded of {@link PiMulticastGroupEntry} to P4Runtime MulticastGroupEntry
- * messages, and vice versa.
- */
-final class MulticastGroupEntryCodec {
-
-    private MulticastGroupEntryCodec() {
-        // Hides constructor.
-    }
-
-    /**
-     * Returns a P4Runtime MulticastGroupEntry message equivalent to the given
-     * PiMulticastGroupEntry.
-     *
-     * @param piEntry PiMulticastGroupEntry
-     * @return P4Runtime MulticastGroupEntry message
-     * @throws CodecException if the PiMulticastGroupEntry cannot be encoded.
-     */
-    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) throws CodecException {
-        final MulticastGroupEntry.Builder msgBuilder = MulticastGroupEntry.newBuilder();
-        msgBuilder.setMulticastGroupId(piEntry.groupId());
-        for (PiPreReplica replica : piEntry.replicas()) {
-            final int p4PortId;
-            try {
-                p4PortId = Math.toIntExact(replica.egressPort().toLong());
-            } catch (ArithmeticException e) {
-                throw new CodecException(format(
-                        "Cannot cast 64bit port value '%s' to 32bit",
-                        replica.egressPort()));
-            }
-            msgBuilder.addReplicas(
-                    Replica.newBuilder()
-                            .setEgressPort(p4PortId)
-                            .setInstance(replica.instanceId())
-                            .build());
-        }
-        return msgBuilder.build();
-    }
-
-    /**
-     * Returns a PiMulticastGroupEntry equivalent to the given P4Runtime
-     * MulticastGroupEntry message.
-     *
-     * @param msg P4Runtime MulticastGroupEntry message
-     * @return PiMulticastGroupEntry
-     */
-    static PiMulticastGroupEntry decode(MulticastGroupEntry msg) {
-        final PiMulticastGroupEntry.Builder piEntryBuilder = PiMulticastGroupEntry.builder();
-        piEntryBuilder.withGroupId(msg.getMulticastGroupId());
-        msg.getReplicasList().stream()
-                .map(r -> new PiPreReplica(
-                        PortNumber.portNumber(r.getEgressPort()), r.getInstance()))
-                .forEach(piEntryBuilder::addReplica);
-        return piEntryBuilder.build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
deleted file mode 100644
index 6ac478f..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
+++ /dev/null
@@ -1,1282 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.ManagedChannel;
-import io.grpc.Metadata;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
-import io.grpc.protobuf.lite.ProtoLiteUtils;
-import io.grpc.stub.ClientCallStreamObserver;
-import io.grpc.stub.StreamObserver;
-import org.onlab.osgi.DefaultServiceDirectory;
-import org.onlab.util.Tools;
-import org.onosproject.grpc.ctl.AbstractGrpcClient;
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiCounterId;
-import org.onosproject.net.pi.model.PiMeterId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiActionProfileGroup;
-import org.onosproject.net.pi.runtime.PiActionProfileMember;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import org.onosproject.net.pi.runtime.PiCounterCell;
-import org.onosproject.net.pi.runtime.PiCounterCellId;
-import org.onosproject.net.pi.runtime.PiMeterCellConfig;
-import org.onosproject.net.pi.runtime.PiMeterCellId;
-import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
-import org.onosproject.net.pi.runtime.PiPacketOperation;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.service.PiPipeconfService;
-import org.onosproject.p4runtime.api.P4RuntimeClient;
-import org.onosproject.p4runtime.api.P4RuntimeClientKey;
-import org.onosproject.p4runtime.api.P4RuntimeEvent;
-import p4.config.v1.P4InfoOuterClass.P4Info;
-import p4.tmp.P4Config;
-import p4.v1.P4RuntimeGrpc;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-import p4.v1.P4RuntimeOuterClass.Entity;
-import p4.v1.P4RuntimeOuterClass.ForwardingPipelineConfig;
-import p4.v1.P4RuntimeOuterClass.GetForwardingPipelineConfigRequest;
-import p4.v1.P4RuntimeOuterClass.GetForwardingPipelineConfigResponse;
-import p4.v1.P4RuntimeOuterClass.MasterArbitrationUpdate;
-import p4.v1.P4RuntimeOuterClass.MulticastGroupEntry;
-import p4.v1.P4RuntimeOuterClass.PacketReplicationEngineEntry;
-import p4.v1.P4RuntimeOuterClass.ReadRequest;
-import p4.v1.P4RuntimeOuterClass.ReadResponse;
-import p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest;
-import p4.v1.P4RuntimeOuterClass.StreamMessageRequest;
-import p4.v1.P4RuntimeOuterClass.StreamMessageResponse;
-import p4.v1.P4RuntimeOuterClass.TableEntry;
-import p4.v1.P4RuntimeOuterClass.Uint128;
-import p4.v1.P4RuntimeOuterClass.Update;
-import p4.v1.P4RuntimeOuterClass.WriteRequest;
-
-import java.math.BigInteger;
-import java.net.ConnectException;
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Stream;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.lang.String.format;
-import static java.util.Collections.singletonList;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
-import static org.onosproject.p4runtime.ctl.P4RuntimeCodecs.CODECS;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_GROUP;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_MEMBER;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_COUNTER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_METER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.METER_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.PACKET_REPLICATION_ENGINE_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.PacketIn;
-import static p4.v1.P4RuntimeOuterClass.PacketOut;
-import static p4.v1.P4RuntimeOuterClass.PacketReplicationEngineEntry.TypeCase.MULTICAST_GROUP_ENTRY;
-import static p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest.Action.VERIFY_AND_COMMIT;
-
-/**
- * Implementation of a P4Runtime client.
- */
-final class P4RuntimeClientImpl extends AbstractGrpcClient implements P4RuntimeClient {
-
-    private static final String MISSING_P4INFO_BROWSER = "Unable to get a P4Info browser for pipeconf {}";
-
-    private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
-            Metadata.Key.of(
-                    "grpc-status-details-bin",
-                    ProtoLiteUtils.metadataMarshaller(
-                            com.google.rpc.Status.getDefaultInstance()));
-
-    private static final Map<WriteOperationType, Update.Type> UPDATE_TYPES = ImmutableMap.of(
-            WriteOperationType.UNSPECIFIED, Update.Type.UNSPECIFIED,
-            WriteOperationType.INSERT, Update.Type.INSERT,
-            WriteOperationType.MODIFY, Update.Type.MODIFY,
-            WriteOperationType.DELETE, Update.Type.DELETE
-    );
-
-    private final long p4DeviceId;
-    private final P4RuntimeControllerImpl controller;
-    private final P4RuntimeGrpc.P4RuntimeBlockingStub blockingStub;
-    private StreamChannelManager streamChannelManager;
-
-    // Used by this client for write requests.
-    private Uint128 clientElectionId = Uint128.newBuilder().setLow(1).build();
-
-    private final AtomicBoolean isClientMaster = new AtomicBoolean(false);
-
-    /**
-     * Default constructor.
-     *
-     * @param clientKey  the client key of this client
-     * @param channel    gRPC channel
-     * @param controller runtime client controller
-     */
-    P4RuntimeClientImpl(P4RuntimeClientKey clientKey, ManagedChannel channel,
-                        P4RuntimeControllerImpl controller) {
-
-        super(clientKey);
-        this.p4DeviceId = clientKey.p4DeviceId();
-        this.controller = controller;
-
-        //TODO Investigate use of stub deadlines instead of timeout in supplyInContext
-        this.blockingStub = P4RuntimeGrpc.newBlockingStub(channel);
-        this.streamChannelManager = new StreamChannelManager(channel);
-    }
-
-    @Override
-    public CompletableFuture<Boolean> startStreamChannel() {
-        return supplyInContext(() -> sendMasterArbitrationUpdate(false),
-                               "start-initStreamChannel");
-    }
-
-    @Override
-    public CompletableFuture<Boolean> becomeMaster() {
-        return supplyInContext(() -> sendMasterArbitrationUpdate(true),
-                               "becomeMaster");
-    }
-
-    @Override
-    public boolean isMaster() {
-        return streamChannelManager.isOpen() && isClientMaster.get();
-    }
-
-    @Override
-    public boolean isStreamChannelOpen() {
-        return streamChannelManager.isOpen();
-    }
-
-    @Override
-    public CompletableFuture<Boolean> setPipelineConfig(PiPipeconf pipeconf, ByteBuffer deviceData) {
-        return supplyInContext(() -> doSetPipelineConfig(pipeconf, deviceData), "setPipelineConfig");
-    }
-
-    @Override
-    public boolean isPipelineConfigSet(PiPipeconf pipeconf, ByteBuffer deviceData) {
-        return doIsPipelineConfigSet(pipeconf, deviceData);
-    }
-
-    @Override
-    public CompletableFuture<Boolean> writeTableEntries(List<PiTableEntry> piTableEntries,
-                                                        WriteOperationType opType, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doWriteTableEntries(piTableEntries, opType, pipeconf),
-                               "writeTableEntries-" + opType.name());
-    }
-
-    @Override
-    public CompletableFuture<List<PiTableEntry>> dumpTables(
-            Set<PiTableId> piTableIds, boolean defaultEntries, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpTables(piTableIds, defaultEntries, pipeconf),
-                               "dumpTables-" + piTableIds.hashCode());
-    }
-
-    @Override
-    public CompletableFuture<List<PiTableEntry>> dumpAllTables(PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpTables(null, false, pipeconf), "dumpAllTables");
-    }
-
-    @Override
-    public CompletableFuture<Boolean> packetOut(PiPacketOperation packet, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doPacketOut(packet, pipeconf), "packetOut");
-    }
-
-    @Override
-    public CompletableFuture<List<PiCounterCell>> readCounterCells(Set<PiCounterCellId> cellIds,
-                                                                   PiPipeconf pipeconf) {
-        return supplyInContext(() -> doReadCounterCells(Lists.newArrayList(cellIds), pipeconf),
-                               "readCounterCells-" + cellIds.hashCode());
-    }
-
-    @Override
-    public CompletableFuture<List<PiCounterCell>> readAllCounterCells(Set<PiCounterId> counterIds,
-                                                                      PiPipeconf pipeconf) {
-        return supplyInContext(() -> doReadAllCounterCells(Lists.newArrayList(counterIds), pipeconf),
-                               "readAllCounterCells-" + counterIds.hashCode());
-    }
-
-    @Override
-    public CompletableFuture<Boolean> writeActionProfileMembers(List<PiActionProfileMember> members,
-                                                                WriteOperationType opType,
-                                                                PiPipeconf pipeconf) {
-        return supplyInContext(() -> doWriteActionProfileMembers(members, opType, pipeconf),
-                               "writeActionProfileMembers-" + opType.name());
-    }
-
-
-    @Override
-    public CompletableFuture<Boolean> writeActionProfileGroup(PiActionProfileGroup group,
-                                                              WriteOperationType opType,
-                                                              PiPipeconf pipeconf) {
-        return supplyInContext(() -> doWriteActionProfileGroup(group, opType, pipeconf),
-                               "writeActionProfileGroup-" + opType.name());
-    }
-
-    @Override
-    public CompletableFuture<List<PiActionProfileGroup>> dumpActionProfileGroups(
-            PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpGroups(actionProfileId, pipeconf),
-                               "dumpActionProfileGroups-" + actionProfileId.id());
-    }
-
-    @Override
-    public CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
-            PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpActionProfileMembers(actionProfileId, pipeconf),
-                               "dumpActionProfileMembers-" + actionProfileId.id());
-    }
-
-    @Override
-    public CompletableFuture<List<PiActionProfileMemberId>> removeActionProfileMembers(
-            PiActionProfileId actionProfileId,
-            List<PiActionProfileMemberId> memberIds,
-            PiPipeconf pipeconf) {
-        return supplyInContext(
-                () -> doRemoveActionProfileMembers(actionProfileId, memberIds, pipeconf),
-                "cleanupActionProfileMembers-" + actionProfileId.id());
-    }
-
-    @Override
-    public CompletableFuture<Boolean> writeMeterCells(List<PiMeterCellConfig> cellIds, PiPipeconf pipeconf) {
-
-        return supplyInContext(() -> doWriteMeterCells(cellIds, pipeconf),
-                               "writeMeterCells");
-    }
-
-    @Override
-    public CompletableFuture<Boolean> writePreMulticastGroupEntries(
-            List<PiMulticastGroupEntry> entries,
-            WriteOperationType opType) {
-        return supplyInContext(() -> doWriteMulticastGroupEntries(entries, opType),
-                               "writePreMulticastGroupEntries");
-    }
-
-    @Override
-    public CompletableFuture<List<PiMulticastGroupEntry>> readAllMulticastGroupEntries() {
-        return supplyInContext(this::doReadAllMulticastGroupEntries,
-                               "readAllMulticastGroupEntries");
-    }
-
-    @Override
-    public CompletableFuture<List<PiMeterCellConfig>> readMeterCells(Set<PiMeterCellId> cellIds,
-                                                                     PiPipeconf pipeconf) {
-        return supplyInContext(() -> doReadMeterCells(Lists.newArrayList(cellIds), pipeconf),
-                               "readMeterCells-" + cellIds.hashCode());
-    }
-
-    @Override
-    public CompletableFuture<List<PiMeterCellConfig>> readAllMeterCells(Set<PiMeterId> meterIds,
-                                                                        PiPipeconf pipeconf) {
-        return supplyInContext(() -> doReadAllMeterCells(Lists.newArrayList(meterIds), pipeconf),
-                               "readAllMeterCells-" + meterIds.hashCode());
-    }
-
-    /* Blocking method implementations below */
-
-    private boolean sendMasterArbitrationUpdate(boolean asMaster) {
-        BigInteger newId = controller.newMasterElectionId(deviceId);
-        if (asMaster) {
-            // Becoming master is a race. Here we increase our chances of win
-            // against other ONOS nodes in the cluster that are calling start()
-            // (which is used to start the stream RPC session, not to become
-            // master).
-            newId = newId.add(BigInteger.valueOf(1000));
-        }
-        final Uint128 idMsg = bigIntegerToUint128(
-                controller.newMasterElectionId(deviceId));
-
-        log.debug("Sending arbitration update to {}... electionId={}",
-                  deviceId, newId);
-
-        streamChannelManager.send(
-                StreamMessageRequest.newBuilder()
-                        .setArbitration(
-                                MasterArbitrationUpdate
-                                        .newBuilder()
-                                        .setDeviceId(p4DeviceId)
-                                        .setElectionId(idMsg)
-                                        .build())
-                        .build());
-        clientElectionId = idMsg;
-        return true;
-    }
-
-    private ForwardingPipelineConfig getPipelineConfig(
-            PiPipeconf pipeconf, ByteBuffer deviceData) {
-        P4Info p4Info = PipeconfHelper.getP4Info(pipeconf);
-        if (p4Info == null) {
-            // Problem logged by PipeconfHelper.
-            return null;
-        }
-
-        ForwardingPipelineConfig.Cookie pipeconfCookie = ForwardingPipelineConfig.Cookie
-                .newBuilder()
-                .setCookie(pipeconf.fingerprint())
-                .build();
-
-        // FIXME: This is specific to PI P4Runtime implementation.
-        P4Config.P4DeviceConfig p4DeviceConfigMsg = P4Config.P4DeviceConfig
-                .newBuilder()
-                .setExtras(P4Config.P4DeviceConfig.Extras.getDefaultInstance())
-                .setReassign(true)
-                .setDeviceData(ByteString.copyFrom(deviceData))
-                .build();
-
-        return ForwardingPipelineConfig
-                .newBuilder()
-                .setP4Info(p4Info)
-                .setP4DeviceConfig(p4DeviceConfigMsg.toByteString())
-                .setCookie(pipeconfCookie)
-                .build();
-    }
-
-    private boolean doIsPipelineConfigSet(PiPipeconf pipeconf, ByteBuffer deviceData) {
-
-        GetForwardingPipelineConfigRequest request = GetForwardingPipelineConfigRequest
-                .newBuilder()
-                .setDeviceId(p4DeviceId)
-                .setResponseType(GetForwardingPipelineConfigRequest
-                                         .ResponseType.COOKIE_ONLY)
-                .build();
-
-        GetForwardingPipelineConfigResponse resp;
-        try {
-            resp = this.blockingStub
-                    .getForwardingPipelineConfig(request);
-        } catch (StatusRuntimeException ex) {
-            checkGrpcException(ex);
-            // FAILED_PRECONDITION means that a pipeline config was not set in
-            // the first place. Don't bother logging.
-            if (!ex.getStatus().getCode()
-                    .equals(Status.FAILED_PRECONDITION.getCode())) {
-                log.warn("Unable to get pipeline config from {}: {}",
-                         deviceId, ex.getMessage());
-            }
-            return false;
-        }
-        if (!resp.getConfig().hasCookie()) {
-            log.warn("{} returned GetForwardingPipelineConfigResponse " +
-                             "with 'cookie' field unset. " +
-                             "Will try by comparing 'device_data'...",
-                     deviceId);
-            return doIsPipelineConfigSetWithData(pipeconf, deviceData);
-        }
-
-        return resp.getConfig().getCookie().getCookie() == pipeconf.fingerprint();
-    }
-
-    private boolean doIsPipelineConfigSetWithData(PiPipeconf pipeconf, ByteBuffer deviceData) {
-
-        GetForwardingPipelineConfigRequest request = GetForwardingPipelineConfigRequest
-                .newBuilder()
-                .setDeviceId(p4DeviceId)
-                .build();
-
-        GetForwardingPipelineConfigResponse resp;
-        try {
-            resp = this.blockingStub
-                    .getForwardingPipelineConfig(request);
-        } catch (StatusRuntimeException ex) {
-            checkGrpcException(ex);
-            return false;
-        }
-
-        ForwardingPipelineConfig expectedConfig = getPipelineConfig(
-                pipeconf, deviceData);
-
-        if (expectedConfig == null) {
-            return false;
-        }
-        if (!resp.hasConfig()) {
-            log.warn("{} returned GetForwardingPipelineConfigResponse " +
-                             "with 'config' field unset",
-                     deviceId);
-            return false;
-        }
-        if (resp.getConfig().getP4DeviceConfig().isEmpty()
-                && !expectedConfig.getP4DeviceConfig().isEmpty()) {
-            // Don't bother with a warn or error since we don't really allow
-            // updating the pipeline to a different one. So the P4Info should be
-            // enough for us.
-            log.debug("{} returned GetForwardingPipelineConfigResponse " +
-                              "with empty 'p4_device_config' field, " +
-                              "equality will be based only on P4Info",
-                      deviceId);
-            return resp.getConfig().getP4Info().equals(
-                    expectedConfig.getP4Info());
-        } else {
-            return resp.getConfig().getP4DeviceConfig()
-                    .equals(expectedConfig.getP4DeviceConfig())
-                    && resp.getConfig().getP4Info()
-                    .equals(expectedConfig.getP4Info());
-        }
-    }
-
-    private boolean doSetPipelineConfig(PiPipeconf pipeconf, ByteBuffer deviceData) {
-
-        log.info("Setting pipeline config for {} to {}...", deviceId, pipeconf.id());
-
-        checkNotNull(deviceData, "deviceData cannot be null");
-
-        ForwardingPipelineConfig pipelineConfig = getPipelineConfig(pipeconf, deviceData);
-
-        if (pipelineConfig == null) {
-            // Error logged in getPipelineConfig()
-            return false;
-        }
-
-        SetForwardingPipelineConfigRequest request = SetForwardingPipelineConfigRequest
-                .newBuilder()
-                .setDeviceId(p4DeviceId)
-                .setElectionId(clientElectionId)
-                .setAction(VERIFY_AND_COMMIT)
-                .setConfig(pipelineConfig)
-                .build();
-
-        try {
-            //noinspection ResultOfMethodCallIgnored
-            this.blockingStub.setForwardingPipelineConfig(request);
-            return true;
-        } catch (StatusRuntimeException ex) {
-            checkGrpcException(ex);
-            log.warn("Unable to set pipeline config on {}: {}", deviceId, ex.getMessage());
-            return false;
-        }
-    }
-
-    private boolean doWriteTableEntries(List<PiTableEntry> piTableEntries, WriteOperationType opType,
-                                        PiPipeconf pipeconf) {
-        if (piTableEntries.size() == 0) {
-            return true;
-        }
-
-        List<Update> updateMsgs;
-        try {
-            updateMsgs = TableEntryEncoder.encode(piTableEntries, pipeconf)
-                    .stream()
-                    .map(tableEntryMsg ->
-                                 Update.newBuilder()
-                                         .setEntity(Entity.newBuilder()
-                                                            .setTableEntry(tableEntryMsg)
-                                                            .build())
-                                         .setType(UPDATE_TYPES.get(opType))
-                                         .build())
-                    .collect(toList());
-        } catch (CodecException e) {
-            log.error("Unable to encode table entries, aborting {} operation: {}",
-                      opType.name(), e.getMessage());
-            return false;
-        }
-
-        return write(updateMsgs, piTableEntries, opType, "table entry");
-    }
-
-    private List<PiTableEntry> doDumpTables(
-            Set<PiTableId> piTableIds, boolean defaultEntries, PiPipeconf pipeconf) {
-
-        log.debug("Dumping tables {} from {} (pipeconf {})...",
-                  piTableIds, deviceId, pipeconf.id());
-
-        Set<Integer> tableIds = Sets.newHashSet();
-        if (piTableIds == null) {
-            // Dump all tables.
-            tableIds.add(0);
-        } else {
-            P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-            if (browser == null) {
-                log.error(MISSING_P4INFO_BROWSER, pipeconf);
-                return Collections.emptyList();
-            }
-            piTableIds.forEach(piTableId -> {
-                try {
-                    tableIds.add(browser.tables().getByName(piTableId.id()).getPreamble().getId());
-                } catch (P4InfoBrowser.NotFoundException e) {
-                    log.warn("Unable to dump table {}: {}", piTableId, e.getMessage());
-                }
-            });
-        }
-
-        if (tableIds.isEmpty()) {
-            return Collections.emptyList();
-        }
-
-        final List<Entity> entities = tableIds.stream()
-                .map(tableId ->  TableEntry.newBuilder()
-                        .setTableId(tableId)
-                        .setIsDefaultAction(defaultEntries)
-                        .setCounterData(P4RuntimeOuterClass.CounterData.getDefaultInstance())
-                        .build())
-                .map(e -> Entity.newBuilder().setTableEntry(e).build())
-                .collect(toList());
-
-        final List<TableEntry> tableEntryMsgs = blockingRead(entities, TABLE_ENTRY)
-                .map(Entity::getTableEntry)
-                .collect(toList());
-
-        log.debug("Retrieved {} entries from {} tables on {}...",
-                  tableEntryMsgs.size(), tableIds.size(), deviceId);
-
-        return TableEntryEncoder.decode(tableEntryMsgs, pipeconf);
-    }
-
-    private boolean doPacketOut(PiPacketOperation packet, PiPipeconf pipeconf) {
-        try {
-            //encode the PiPacketOperation into a PacketOut
-            PacketOut packetOut = PacketIOCodec.encodePacketOut(packet, pipeconf);
-
-            //Build the request
-            StreamMessageRequest packetOutRequest = StreamMessageRequest
-                    .newBuilder().setPacket(packetOut).build();
-
-            //Send the request
-            streamChannelManager.send(packetOutRequest);
-
-        } catch (P4InfoBrowser.NotFoundException e) {
-            log.error("Cant find expected metadata in p4Info file. {}", e.getMessage());
-            log.debug("Exception", e);
-            return false;
-        }
-        return true;
-    }
-
-    private void doPacketIn(PacketIn packetInMsg) {
-
-        // Retrieve the pipeconf for this client's device.
-        PiPipeconfService pipeconfService = DefaultServiceDirectory.getService(PiPipeconfService.class);
-        if (pipeconfService == null) {
-            throw new IllegalStateException("PiPipeconfService is null. Can't handle packet in.");
-        }
-        final PiPipeconf pipeconf;
-        if (pipeconfService.ofDevice(deviceId).isPresent() &&
-                pipeconfService.getPipeconf(pipeconfService.ofDevice(deviceId).get()).isPresent()) {
-            pipeconf = pipeconfService.getPipeconf(pipeconfService.ofDevice(deviceId).get()).get();
-        } else {
-            log.warn("Unable to get pipeconf of {}. Can't handle packet in", deviceId);
-            return;
-        }
-        // Decode packet message and post event.
-        PiPacketOperation packetOperation = PacketIOCodec.decodePacketIn(packetInMsg, pipeconf, deviceId);
-        PacketInEvent packetInEventSubject = new PacketInEvent(deviceId, packetOperation);
-        P4RuntimeEvent event = new P4RuntimeEvent(P4RuntimeEvent.Type.PACKET_IN, packetInEventSubject);
-        log.debug("Received packet in: {}", event);
-        controller.postEvent(event);
-    }
-
-    private void doArbitrationResponse(MasterArbitrationUpdate msg) {
-        // From the spec...
-        // - Election_id: The stream RPC with the highest election_id is the
-        // master. Switch populates with the highest election ID it
-        // has received from all connected controllers.
-        // - Status: Switch populates this with OK for the client that is the
-        // master, and with an error status for all other connected clients (at
-        // every mastership change).
-        if (!msg.hasElectionId() || !msg.hasStatus()) {
-            return;
-        }
-        final boolean isMaster =
-                msg.getStatus().getCode() == Status.OK.getCode().value();
-        log.debug("Received arbitration update from {}: isMaster={}, electionId={}",
-                  deviceId, isMaster, uint128ToBigInteger(msg.getElectionId()));
-        controller.postEvent(new P4RuntimeEvent(
-                P4RuntimeEvent.Type.ARBITRATION_RESPONSE,
-                new ArbitrationResponse(deviceId, isMaster)));
-        isClientMaster.set(isMaster);
-    }
-
-    private List<PiCounterCell> doReadAllCounterCells(
-            List<PiCounterId> counterIds, PiPipeconf pipeconf) {
-        return doReadCounterEntities(
-                CounterEntryCodec.readAllCellsEntities(counterIds, pipeconf),
-                pipeconf);
-    }
-
-    private List<PiCounterCell> doReadCounterCells(
-            List<PiCounterCellId> cellIds, PiPipeconf pipeconf) {
-        return doReadCounterEntities(
-                CounterEntryCodec.encodePiCounterCellIds(cellIds, pipeconf),
-                pipeconf);
-    }
-
-    private List<PiCounterCell> doReadCounterEntities(
-            List<Entity> counterEntities, PiPipeconf pipeconf) {
-
-        final List<Entity> entities = blockingRead(
-                counterEntities, COUNTER_ENTRY, DIRECT_COUNTER_ENTRY)
-                .collect(toList());
-
-        return CounterEntryCodec.decodeCounterEntities(entities, pipeconf);
-    }
-
-    private boolean doWriteActionProfileMembers(List<PiActionProfileMember> members,
-                                                WriteOperationType opType, PiPipeconf pipeconf) {
-        final List<ActionProfileMember> actionProfileMembers;
-        try {
-            actionProfileMembers = CODECS.actionProfileMember()
-                    .encodeAllOrFail(members, pipeconf);
-        } catch (CodecException e) {
-            log.warn("Unable to {} action profile members: {}",
-                     opType.name(), e.getMessage());
-            return false;
-        }
-        final List<Update> updateMsgs = actionProfileMembers.stream()
-                .map(m -> Update.newBuilder()
-                        .setEntity(Entity.newBuilder()
-                                           .setActionProfileMember(m)
-                                           .build())
-                        .setType(UPDATE_TYPES.get(opType))
-                        .build())
-                .collect(toList());
-        return write(updateMsgs, members, opType, "action profile member");
-    }
-
-    private List<PiActionProfileGroup> doDumpGroups(PiActionProfileId piActionProfileId, PiPipeconf pipeconf) {
-        log.debug("Dumping groups from action profile {} from {} (pipeconf {})...",
-                  piActionProfileId.id(), deviceId, pipeconf.id());
-
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            log.warn(MISSING_P4INFO_BROWSER, pipeconf);
-            return Collections.emptyList();
-        }
-
-        final int actionProfileId;
-        try {
-            actionProfileId = browser
-                    .actionProfiles()
-                    .getByName(piActionProfileId.id())
-                    .getPreamble()
-                    .getId();
-        } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to dump groups: {}", e.getMessage());
-            return Collections.emptyList();
-        }
-
-        // Read all groups from the given action profile.
-        final Entity entityToRead = Entity.newBuilder()
-                .setActionProfileGroup(
-                        ActionProfileGroup.newBuilder()
-                                .setActionProfileId(actionProfileId)
-                                .build())
-                .build();
-        final List<ActionProfileGroup> groupMsgs = blockingRead(entityToRead, ACTION_PROFILE_GROUP)
-                .map(Entity::getActionProfileGroup)
-                .collect(toList());
-
-        log.debug("Retrieved {} groups from action profile {} on {}...",
-                  groupMsgs.size(), piActionProfileId.id(), deviceId);
-
-        return CODECS.actionProfileGroup().decodeAll(groupMsgs, pipeconf);
-    }
-
-    private List<PiActionProfileMember> doDumpActionProfileMembers(
-            PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
-
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            log.error(MISSING_P4INFO_BROWSER, pipeconf);
-            return Collections.emptyList();
-        }
-
-        final int p4ActProfId;
-        try {
-            p4ActProfId = browser
-                    .actionProfiles()
-                    .getByName(actionProfileId.id())
-                    .getPreamble()
-                    .getId();
-        } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to dump action profile members: {}", e.getMessage());
-            return Collections.emptyList();
-        }
-
-        Entity entityToRead = Entity.newBuilder()
-                .setActionProfileMember(
-                        ActionProfileMember.newBuilder()
-                                .setActionProfileId(p4ActProfId)
-                                .build())
-                .build();
-        final List<ActionProfileMember> memberMsgs = blockingRead(entityToRead, ACTION_PROFILE_MEMBER)
-                .map(Entity::getActionProfileMember)
-                .collect(toList());
-
-        log.debug("Retrieved {} members from action profile {} on {}...",
-                  memberMsgs.size(), actionProfileId.id(), deviceId);
-
-        return CODECS.actionProfileMember().decodeAll(memberMsgs, pipeconf);
-    }
-
-    private List<PiActionProfileMemberId> doRemoveActionProfileMembers(
-            PiActionProfileId actionProfileId,
-            List<PiActionProfileMemberId> memberIds,
-            PiPipeconf pipeconf) {
-
-        if (memberIds.isEmpty()) {
-            return Collections.emptyList();
-        }
-
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            log.error(MISSING_P4INFO_BROWSER, pipeconf);
-            return Collections.emptyList();
-        }
-
-        final int p4ActProfId;
-        try {
-            p4ActProfId = browser.actionProfiles()
-                    .getByName(actionProfileId.id()).getPreamble().getId();
-        } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to cleanup action profile members: {}", e.getMessage());
-            return Collections.emptyList();
-        }
-
-        final List<Update> updateMsgs = memberIds.stream()
-                .map(m -> ActionProfileMember.newBuilder()
-                        .setActionProfileId(p4ActProfId)
-                        .setMemberId(m.id()).build())
-                .map(m -> Entity.newBuilder().setActionProfileMember(m).build())
-                .map(e -> Update.newBuilder().setEntity(e)
-                        .setType(Update.Type.DELETE).build())
-                .collect(toList());
-
-        log.debug("Removing {} members of action profile '{}'...",
-                  memberIds.size(), actionProfileId);
-
-        return writeAndReturnSuccessEntities(
-                updateMsgs, memberIds, WriteOperationType.DELETE,
-                "action profile members");
-    }
-
-    private boolean doWriteActionProfileGroup(
-            PiActionProfileGroup group, WriteOperationType opType, PiPipeconf pipeconf) {
-        final ActionProfileGroup groupMsg;
-        try {
-            groupMsg = CODECS.actionProfileGroup().encode(group, pipeconf);
-        } catch (CodecException e) {
-            log.warn("Unable to encode group, aborting {} operation: {}",
-                     opType.name(), e.getMessage());
-            return false;
-        }
-
-        final Update updateMsg = Update.newBuilder()
-                .setEntity(Entity.newBuilder()
-                                   .setActionProfileGroup(groupMsg)
-                                   .build())
-                .setType(UPDATE_TYPES.get(opType))
-                .build();
-
-        return write(singletonList(updateMsg), singletonList(group),
-                     opType, "group");
-    }
-
-    private List<PiMeterCellConfig> doReadAllMeterCells(
-            List<PiMeterId> meterIds, PiPipeconf pipeconf) {
-        return doReadMeterEntities(MeterEntryCodec.readAllCellsEntities(
-                meterIds, pipeconf), pipeconf);
-    }
-
-    private List<PiMeterCellConfig> doReadMeterCells(
-            List<PiMeterCellId> cellIds, PiPipeconf pipeconf) {
-
-        final List<PiMeterCellConfig> piMeterCellConfigs = cellIds.stream()
-                .map(cellId -> PiMeterCellConfig.builder()
-                        .withMeterCellId(cellId)
-                        .build())
-                .collect(toList());
-
-        return doReadMeterEntities(MeterEntryCodec.encodePiMeterCellConfigs(
-                piMeterCellConfigs, pipeconf), pipeconf);
-    }
-
-    private List<PiMeterCellConfig> doReadMeterEntities(
-            List<Entity> entitiesToRead, PiPipeconf pipeconf) {
-
-        final List<Entity> responseEntities = blockingRead(
-                entitiesToRead, METER_ENTRY, DIRECT_METER_ENTRY)
-                .collect(toList());
-
-        return MeterEntryCodec.decodeMeterEntities(responseEntities, pipeconf);
-    }
-
-    private boolean doWriteMeterCells(List<PiMeterCellConfig> cellConfigs, PiPipeconf pipeconf) {
-
-        List<Update> updateMsgs = MeterEntryCodec.encodePiMeterCellConfigs(cellConfigs, pipeconf)
-                .stream()
-                .map(meterEntryMsg ->
-                             Update.newBuilder()
-                                     .setEntity(meterEntryMsg)
-                                     .setType(UPDATE_TYPES.get(WriteOperationType.MODIFY))
-                                     .build())
-                .collect(toList());
-
-        if (updateMsgs.size() == 0) {
-            return true;
-        }
-
-        return write(updateMsgs, cellConfigs, WriteOperationType.MODIFY, "meter cell config");
-    }
-
-    private boolean doWriteMulticastGroupEntries(
-            List<PiMulticastGroupEntry> entries,
-            WriteOperationType opType) {
-
-        final List<Update> updateMsgs = entries.stream()
-                .map(piEntry -> {
-                    try {
-                        return MulticastGroupEntryCodec.encode(piEntry);
-                    } catch (CodecException e) {
-                        log.warn("Unable to encode PiMulticastGroupEntry: {}", e.getMessage());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .map(mcMsg -> PacketReplicationEngineEntry.newBuilder()
-                        .setMulticastGroupEntry(mcMsg)
-                        .build())
-                .map(preMsg -> Entity.newBuilder()
-                        .setPacketReplicationEngineEntry(preMsg)
-                        .build())
-                .map(entityMsg -> Update.newBuilder()
-                        .setEntity(entityMsg)
-                        .setType(UPDATE_TYPES.get(opType))
-                        .build())
-                .collect(toList());
-        return write(updateMsgs, entries, opType, "multicast group entry");
-    }
-
-    private List<PiMulticastGroupEntry> doReadAllMulticastGroupEntries() {
-
-        final Entity entity = Entity.newBuilder()
-                .setPacketReplicationEngineEntry(
-                        PacketReplicationEngineEntry.newBuilder()
-                                .setMulticastGroupEntry(
-                                        MulticastGroupEntry.newBuilder()
-                                                .build())
-                                .build())
-                .build();
-
-        final List<PiMulticastGroupEntry> mcEntries = blockingRead(entity, PACKET_REPLICATION_ENGINE_ENTRY)
-                .map(Entity::getPacketReplicationEngineEntry)
-                .filter(e -> e.getTypeCase().equals(MULTICAST_GROUP_ENTRY))
-                .map(PacketReplicationEngineEntry::getMulticastGroupEntry)
-                .map(MulticastGroupEntryCodec::decode)
-                .collect(toList());
-
-        log.debug("Retrieved {} multicast group entries from {}...",
-                  mcEntries.size(), deviceId);
-
-        return mcEntries;
-    }
-
-    private <T> boolean write(List<Update> updates,
-                              List<T> writeEntities,
-                              WriteOperationType opType,
-                              String entryType) {
-        // True if all entities were successfully written.
-        return writeAndReturnSuccessEntities(updates, writeEntities, opType, entryType)
-                .size() == writeEntities.size();
-    }
-
-    private <T> List<T> writeAndReturnSuccessEntities(
-            List<Update> updates, List<T> writeEntities,
-            WriteOperationType opType, String entryType) {
-        if (updates.isEmpty()) {
-            return Collections.emptyList();
-        }
-        if (updates.size() != writeEntities.size()) {
-            log.error("Cannot perform {} operation, provided {} " +
-                              "update messages for {} {} - BUG?",
-                      opType, updates.size(), writeEntities.size(), entryType);
-            return Collections.emptyList();
-        }
-        try {
-            //noinspection ResultOfMethodCallIgnored
-            blockingStub.write(writeRequest(updates));
-            return writeEntities;
-        } catch (StatusRuntimeException e) {
-            return checkAndLogWriteErrors(writeEntities, e, opType, entryType);
-        }
-    }
-
-    private WriteRequest writeRequest(Iterable<Update> updateMsgs) {
-        return WriteRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .setElectionId(clientElectionId)
-                .addAllUpdates(updateMsgs)
-                .build();
-    }
-
-    private Stream<Entity> blockingRead(Entity entity, Entity.EntityCase entityCase) {
-        return blockingRead(singletonList(entity), entityCase);
-    }
-
-    private Stream<Entity> blockingRead(Iterable<Entity> entities,
-                                        Entity.EntityCase... entityCases) {
-        // Build read request making sure we are reading what declared.
-        final ReadRequest.Builder reqBuilder = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId);
-        final Set<Entity.EntityCase> entityCaseSet = Sets.newHashSet(entityCases);
-        for (Entity e : entities) {
-            checkArgument(entityCaseSet.contains(e.getEntityCase()),
-                          "Entity case mismatch");
-            reqBuilder.addEntities(e);
-        }
-        final ReadRequest readRequest = reqBuilder.build();
-        if (readRequest.getEntitiesCount() == 0) {
-            return Stream.empty();
-        }
-        // Issue read.
-        final Iterator<ReadResponse> responseIterator;
-        try {
-            responseIterator = blockingStub.read(readRequest);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            final String caseString = entityCaseSet.stream()
-                    .map(Entity.EntityCase::name)
-                    .collect(joining("/"));
-            log.warn("Unable to read {} from {}: {}",
-                     caseString, deviceId, e.getMessage());
-            log.debug("Exception during read", e);
-            return Stream.empty();
-        }
-        // Filter results.
-        return Tools.stream(() -> responseIterator)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> entityCaseSet.contains(e.getEntityCase()));
-    }
-
-    protected Void doShutdown() {
-        streamChannelManager.complete();
-        return super.doShutdown();
-    }
-
-    // Returns the collection of succesfully write entities.
-    private <T> List<T> checkAndLogWriteErrors(
-            List<T> writeEntities, StatusRuntimeException ex,
-            WriteOperationType opType, String entryType) {
-
-        checkGrpcException(ex);
-
-        final List<P4RuntimeOuterClass.Error> errors = extractWriteErrorDetails(ex);
-
-        if (errors.isEmpty()) {
-            final String description = ex.getStatus().getDescription();
-            log.warn("Unable to {} {} {}(s) on {}: {}",
-                     opType.name(), writeEntities.size(), entryType, deviceId,
-                     ex.getStatus().getCode().name(),
-                     description == null ? "" : " - " + description);
-            return Collections.emptyList();
-        }
-
-        if (errors.size() == writeEntities.size()) {
-            List<T> okEntities = Lists.newArrayList();
-            Iterator<T> entityIterator = writeEntities.iterator();
-            for (P4RuntimeOuterClass.Error error : errors) {
-                T entity = entityIterator.next();
-                if (error.getCanonicalCode() != Status.OK.getCode().value()) {
-                    log.warn("Unable to {} {} on {}: {} [{}]",
-                             opType.name(), entryType, deviceId,
-                             parseP4Error(error), entity.toString());
-                } else {
-                    okEntities.add(entity);
-                }
-            }
-            return okEntities;
-        } else {
-            log.warn("Unable to reconcile error details to {} updates " +
-                             "(sent {} updates, but device returned {} errors)",
-                     entryType, writeEntities.size(), errors.size());
-            errors.stream()
-                    .filter(err -> err.getCanonicalCode() != Status.OK.getCode().value())
-                    .forEach(err -> log.warn("Unable to {} {} (unknown): {}",
-                                             opType.name(), entryType, parseP4Error(err)));
-            return Collections.emptyList();
-        }
-    }
-
-    private List<P4RuntimeOuterClass.Error> extractWriteErrorDetails(
-            StatusRuntimeException ex) {
-        if (!ex.getTrailers().containsKey(STATUS_DETAILS_KEY)) {
-            return Collections.emptyList();
-        }
-        com.google.rpc.Status status = ex.getTrailers().get(STATUS_DETAILS_KEY);
-        if (status == null) {
-            return Collections.emptyList();
-        }
-        return status.getDetailsList().stream()
-                .map(any -> {
-                    try {
-                        return any.unpack(P4RuntimeOuterClass.Error.class);
-                    } catch (InvalidProtocolBufferException e) {
-                        log.warn("Unable to unpack P4Runtime Error: {}",
-                                 any.toString());
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(toList());
-    }
-
-    private String parseP4Error(P4RuntimeOuterClass.Error err) {
-        return format("%s %s%s (%s:%d)",
-                      Status.fromCodeValue(err.getCanonicalCode()).getCode(),
-                      err.getMessage(),
-                      err.hasDetails() ? ", " + err.getDetails().toString() : "",
-                      err.getSpace(),
-                      err.getCode());
-    }
-
-    private void checkGrpcException(StatusRuntimeException ex) {
-        switch (ex.getStatus().getCode()) {
-            case OK:
-                break;
-            case CANCELLED:
-                break;
-            case UNKNOWN:
-                break;
-            case INVALID_ARGUMENT:
-                break;
-            case DEADLINE_EXCEEDED:
-                break;
-            case NOT_FOUND:
-                break;
-            case ALREADY_EXISTS:
-                break;
-            case PERMISSION_DENIED:
-                // Notify upper layers that this node is not master.
-                controller.postEvent(new P4RuntimeEvent(
-                        P4RuntimeEvent.Type.PERMISSION_DENIED,
-                        new BaseP4RuntimeEventSubject(deviceId)));
-                break;
-            case RESOURCE_EXHAUSTED:
-                break;
-            case FAILED_PRECONDITION:
-                break;
-            case ABORTED:
-                break;
-            case OUT_OF_RANGE:
-                break;
-            case UNIMPLEMENTED:
-                break;
-            case INTERNAL:
-                break;
-            case UNAVAILABLE:
-                // Channel might be closed.
-                controller.postEvent(new P4RuntimeEvent(
-                        P4RuntimeEvent.Type.CHANNEL_EVENT,
-                        new ChannelEvent(deviceId, ChannelEvent.Type.ERROR)));
-                break;
-            case DATA_LOSS:
-                break;
-            case UNAUTHENTICATED:
-                break;
-            default:
-                break;
-        }
-    }
-
-    private Uint128 bigIntegerToUint128(BigInteger value) {
-        final byte[] arr = value.toByteArray();
-        final ByteBuffer bb = ByteBuffer.allocate(Long.BYTES * 2)
-                .put(new byte[Long.BYTES * 2 - arr.length])
-                .put(arr);
-        bb.rewind();
-        return Uint128.newBuilder()
-                .setHigh(bb.getLong())
-                .setLow(bb.getLong())
-                .build();
-    }
-
-    private BigInteger uint128ToBigInteger(Uint128 value) {
-        return new BigInteger(
-                ByteBuffer.allocate(Long.BYTES * 2)
-                        .putLong(value.getHigh())
-                        .putLong(value.getLow())
-                        .array());
-    }
-
-    /**
-     * A manager for the P4Runtime stream channel that opportunistically creates
-     * new stream RCP stubs (e.g. when one fails because of errors) and posts
-     * channel events via the P4Runtime controller.
-     */
-    private final class StreamChannelManager {
-
-        private final ManagedChannel channel;
-        private final AtomicBoolean open;
-        private final StreamObserver<StreamMessageResponse> responseObserver;
-        private ClientCallStreamObserver<StreamMessageRequest> requestObserver;
-
-        private StreamChannelManager(ManagedChannel channel) {
-            this.channel = channel;
-            this.responseObserver = new InternalStreamResponseObserver(this);
-            this.open = new AtomicBoolean(false);
-        }
-
-        private void initIfRequired() {
-            if (requestObserver == null) {
-                log.debug("Creating new stream channel for {}...", deviceId);
-                requestObserver =
-                        (ClientCallStreamObserver<StreamMessageRequest>)
-                                P4RuntimeGrpc.newStub(channel)
-                                        .streamChannel(responseObserver);
-                open.set(false);
-            }
-        }
-
-        public boolean send(StreamMessageRequest value) {
-            synchronized (this) {
-                initIfRequired();
-                try {
-                    requestObserver.onNext(value);
-                    // FIXME
-                    // signalOpen();
-                    return true;
-                } catch (Throwable ex) {
-                    if (ex instanceof StatusRuntimeException) {
-                        log.warn("Unable to send {} to {}: {}",
-                                 value.getUpdateCase().toString(), deviceId, ex.getMessage());
-                    } else {
-                        log.warn(format(
-                                "Exception while sending %s to %s",
-                                value.getUpdateCase().toString(), deviceId), ex);
-                    }
-                    complete();
-                    return false;
-                }
-            }
-        }
-
-        public void complete() {
-            synchronized (this) {
-                signalClosed();
-                if (requestObserver != null) {
-                    requestObserver.onCompleted();
-                    requestObserver.cancel("Terminated", null);
-                    requestObserver = null;
-                }
-            }
-        }
-
-        void signalOpen() {
-            synchronized (this) {
-                final boolean wasOpen = open.getAndSet(true);
-                if (!wasOpen) {
-                    controller.postEvent(new P4RuntimeEvent(
-                            P4RuntimeEvent.Type.CHANNEL_EVENT,
-                            new ChannelEvent(deviceId, ChannelEvent.Type.OPEN)));
-                }
-            }
-        }
-
-        void signalClosed() {
-            synchronized (this) {
-                final boolean wasOpen = open.getAndSet(false);
-                if (wasOpen) {
-                    controller.postEvent(new P4RuntimeEvent(
-                            P4RuntimeEvent.Type.CHANNEL_EVENT,
-                            new ChannelEvent(deviceId, ChannelEvent.Type.CLOSED)));
-                }
-            }
-        }
-
-        public boolean isOpen() {
-            return open.get();
-        }
-    }
-
-    /**
-     * Handles messages received from the device on the stream channel.
-     */
-    private final class InternalStreamResponseObserver
-            implements StreamObserver<StreamMessageResponse> {
-
-        private final StreamChannelManager streamChannelManager;
-
-        private InternalStreamResponseObserver(
-                StreamChannelManager streamChannelManager) {
-            this.streamChannelManager = streamChannelManager;
-        }
-
-        @Override
-        public void onNext(StreamMessageResponse message) {
-            streamChannelManager.signalOpen();
-            executorService.submit(() -> doNext(message));
-        }
-
-        private void doNext(StreamMessageResponse message) {
-            try {
-                log.debug("Received message on stream channel from {}: {}",
-                          deviceId, message.getUpdateCase());
-                switch (message.getUpdateCase()) {
-                    case PACKET:
-                        doPacketIn(message.getPacket());
-                        return;
-                    case ARBITRATION:
-                        doArbitrationResponse(message.getArbitration());
-                        return;
-                    default:
-                        log.warn("Unrecognized stream message from {}: {}",
-                                 deviceId, message.getUpdateCase());
-                }
-            } catch (Throwable ex) {
-                log.error("Exception while processing stream message from {}",
-                          deviceId, ex);
-            }
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            if (throwable instanceof StatusRuntimeException) {
-                StatusRuntimeException sre = (StatusRuntimeException) throwable;
-                if (sre.getStatus().getCause() instanceof ConnectException) {
-                    log.warn("Device {} is unreachable ({})",
-                             deviceId, sre.getCause().getMessage());
-                } else {
-                    log.warn("Received error on stream channel for {}: {}",
-                             deviceId, throwable.getMessage());
-                }
-            } else {
-                log.warn(format("Received exception on stream channel for %s",
-                                deviceId), throwable);
-            }
-            streamChannelManager.complete();
-        }
-
-        @Override
-        public void onCompleted() {
-            log.warn("Stream channel for {} has completed", deviceId);
-            streamChannelManager.complete();
-        }
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
deleted file mode 100644
index 1126fef..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2019-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-/**
- * Utility class that provides access to P4Runtime codec instances.
- */
-final class P4RuntimeCodecs {
-
-    static final P4RuntimeCodecs CODECS = new P4RuntimeCodecs();
-
-    private final ActionProfileMemberCodec actionProfileMember;
-    private final ActionProfileGroupCodec actionProfileGroup;
-
-    private P4RuntimeCodecs() {
-        this.actionProfileMember = new ActionProfileMemberCodec();
-        this.actionProfileGroup = new ActionProfileGroupCodec();
-    }
-
-    ActionProfileMemberCodec actionProfileMember() {
-        return actionProfileMember;
-    }
-
-    ActionProfileGroupCodec actionProfileGroup() {
-        return actionProfileGroup;
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java
deleted file mode 100644
index 726e092..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketIOCodec.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import com.google.protobuf.ByteString;
-import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.pi.model.PiControlMetadataId;
-import org.onosproject.net.pi.model.PiPacketOperationType;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiControlMetadata;
-import org.onosproject.net.pi.runtime.PiPacketOperation;
-import org.slf4j.Logger;
-import p4.config.v1.P4InfoOuterClass;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onosproject.p4runtime.ctl.P4InfoBrowser.NotFoundException;
-import static org.slf4j.LoggerFactory.getLogger;
-import static p4.v1.P4RuntimeOuterClass.PacketIn;
-import static p4.v1.P4RuntimeOuterClass.PacketMetadata;
-import static p4.v1.P4RuntimeOuterClass.PacketOut;
-
-/**
- * Encoder of packet metadata, from ONOS Pi* format, to P4Runtime protobuf messages, and vice versa.
- */
-final class PacketIOCodec {
-
-    private static final Logger log = getLogger(PacketIOCodec.class);
-
-    private static final String PACKET_OUT = "packet_out";
-
-    private static final String PACKET_IN = "packet_in";
-
-    // TODO: implement cache of encoded entities.
-
-    private PacketIOCodec() {
-        // hide.
-    }
-
-    /**
-     * Returns a P4Runtime packet out protobuf message, encoded from the given PiPacketOperation for the given pipeconf.
-     * If a PI packet metadata inside the PacketOperation cannot be encoded, it is skipped, hence the returned PacketOut
-     * collection of metadatas might have different size than the input one.
-     * <p>
-     * Please check the log for an explanation of any error that might have occurred.
-     *
-     * @param packet   PI packet operation
-     * @param pipeconf the pipeconf for the program on the switch
-     * @return a P4Runtime packet out protobuf message
-     * @throws NotFoundException if the browser can't find the packet_out in the given p4Info
-     */
-    static PacketOut encodePacketOut(PiPacketOperation packet, PiPipeconf pipeconf)
-            throws NotFoundException {
-
-        //Get the P4browser
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        //Get the packet out controller packet metadata
-        P4InfoOuterClass.ControllerPacketMetadata controllerControlMetadata =
-                browser.controllerPacketMetadatas().getByName(PACKET_OUT);
-        PacketOut.Builder packetOutBuilder = PacketOut.newBuilder();
-
-        //outer controller packet metadata id
-        int controllerControlMetadataId = controllerControlMetadata.getPreamble().getId();
-
-        //Add all its metadata to the packet out
-        packetOutBuilder.addAllMetadata(encodeControlMetadata(packet, browser, controllerControlMetadataId));
-
-        //Set the packet out payload
-        packetOutBuilder.setPayload(ByteString.copyFrom(packet.data().asReadOnlyBuffer()));
-        return packetOutBuilder.build();
-
-    }
-
-    private static List<PacketMetadata> encodeControlMetadata(PiPacketOperation packet,
-                                                              P4InfoBrowser browser, int controllerControlMetadataId) {
-        return packet.metadatas().stream().map(metadata -> {
-            try {
-                //get each metadata id
-                int metadataId = browser.packetMetadatas(controllerControlMetadataId)
-                        .getByName(metadata.id().toString()).getId();
-
-                //Add the metadata id and it's data the packet out
-                return PacketMetadata.newBuilder()
-                        .setMetadataId(metadataId)
-                        .setValue(ByteString.copyFrom(metadata.value().asReadOnlyBuffer()))
-                        .build();
-            } catch (NotFoundException e) {
-                log.error("Cant find metadata with name {} in p4Info file.", metadata.id());
-                return null;
-            }
-        }).filter(Objects::nonNull).collect(Collectors.toList());
-    }
-
-    /**
-     * Returns a PiPacketOperation, decoded from the given P4Runtime PacketIn protobuf message for the given pipeconf
-     * and device ID. If a PI packet metadata inside the protobuf message cannot be decoded, it is skipped, hence the
-     * returned PiPacketOperation collection of metadatas might have different size than the input one.
-     * <p>
-     * Please check the log for an explanation of any error that might have occurred.
-     *
-     * @param packetIn the P4Runtime PacketIn message
-     * @param pipeconf the pipeconf for the program on the switch
-     * @param deviceId the deviceId that originated the PacketIn message
-     * @return a PiPacketOperation
-     */
-    static PiPacketOperation decodePacketIn(PacketIn packetIn, PiPipeconf pipeconf, DeviceId deviceId) {
-
-        //Get the P4browser
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        List<PiControlMetadata> packetMetadatas;
-        try {
-            int controllerControlMetadataId = browser.controllerPacketMetadatas().getByName(PACKET_IN)
-                    .getPreamble().getId();
-            packetMetadatas = decodeControlMetadataIn(packetIn.getMetadataList(), browser,
-                                                      controllerControlMetadataId);
-        } catch (NotFoundException e) {
-            log.error("Unable to decode packet metadatas: {}", e.getMessage());
-            packetMetadatas = Collections.emptyList();
-        }
-
-        //Transform the packetIn data
-        ImmutableByteSequence data = copyFrom(packetIn.getPayload().asReadOnlyByteBuffer());
-
-        //Build the PiPacketOperation with all the metadatas.
-        return PiPacketOperation.builder()
-                .forDevice(deviceId)
-                .withType(PiPacketOperationType.PACKET_IN)
-                .withMetadatas(packetMetadatas)
-                .withData(data)
-                .build();
-    }
-
-    private static List<PiControlMetadata> decodeControlMetadataIn(List<PacketMetadata> packetMetadatas,
-                                                                   P4InfoBrowser browser,
-                                                                   int controllerControlMetadataId) {
-        return packetMetadatas.stream().map(packetMetadata -> {
-            try {
-
-                int packetMetadataId = packetMetadata.getMetadataId();
-                String packetMetadataName = browser.packetMetadatas(controllerControlMetadataId)
-                        .getById(packetMetadataId).getName();
-
-                PiControlMetadataId metadataId = PiControlMetadataId.of(packetMetadataName);
-
-                //Build each metadata.
-                return PiControlMetadata.builder()
-                        .withId(metadataId)
-                        .withValue(ImmutableByteSequence.copyFrom(packetMetadata.getValue().asReadOnlyByteBuffer()))
-                        .build();
-            } catch (NotFoundException e) {
-                log.error("Cant find metadata with id {} in p4Info file.", packetMetadata.getMetadataId());
-                return null;
-            }
-        }).filter(Objects::nonNull).collect(Collectors.toList());
-    }
-
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
deleted file mode 100644
index 357e41d..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.ctl;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.protobuf.ByteString;
-import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.net.pi.model.PiActionId;
-import org.onosproject.net.pi.model.PiActionParamId;
-import org.onosproject.net.pi.model.PiMatchFieldId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiAction;
-import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import org.onosproject.net.pi.runtime.PiCounterCellData;
-import org.onosproject.net.pi.runtime.PiExactFieldMatch;
-import org.onosproject.net.pi.runtime.PiFieldMatch;
-import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
-import org.onosproject.net.pi.runtime.PiMatchKey;
-import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
-import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
-import org.slf4j.Logger;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass.Action;
-import p4.v1.P4RuntimeOuterClass.CounterData;
-import p4.v1.P4RuntimeOuterClass.FieldMatch;
-import p4.v1.P4RuntimeOuterClass.TableAction;
-import p4.v1.P4RuntimeOuterClass.TableEntry;
-
-import java.util.Collections;
-import java.util.List;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.lang.String.format;
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onosproject.p4runtime.ctl.P4RuntimeUtils.assertPrefixLen;
-import static org.onosproject.p4runtime.ctl.P4RuntimeUtils.assertSize;
-import static org.slf4j.LoggerFactory.getLogger;
-
-/**
- * Encoder/Decoder of table entries, from ONOS Pi* format, to P4Runtime protobuf messages, and vice versa.
- */
-final class TableEntryEncoder {
-    private static final Logger log = getLogger(TableEntryEncoder.class);
-
-    private static final String VALUE_OF_PREFIX = "value of ";
-    private static final String MASK_OF_PREFIX = "mask of ";
-    private static final String HIGH_RANGE_VALUE_OF_PREFIX = "high range value of ";
-    private static final String LOW_RANGE_VALUE_OF_PREFIX = "low range value of ";
-
-    // TODO: implement cache of encoded entities.
-
-    private TableEntryEncoder() {
-        // hide.
-    }
-
-    /**
-     * Returns a collection of P4Runtime table entry protobuf messages, encoded
-     * from the given collection of PI table entries for the given pipeconf. If
-     * a PI table entry cannot be encoded, an EncodeException is thrown.
-     *
-     * @param piTableEntries PI table entries
-     * @param pipeconf       PI pipeconf
-     * @return collection of P4Runtime table entry protobuf messages
-     * @throws CodecException if a PI table entry cannot be encoded
-     */
-    static List<TableEntry> encode(List<PiTableEntry> piTableEntries,
-                                                PiPipeconf pipeconf)
-            throws CodecException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            throw new CodecException(format(
-                    "Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
-        }
-
-        ImmutableList.Builder<TableEntry> tableEntryMsgListBuilder = ImmutableList.builder();
-
-        for (PiTableEntry piTableEntry : piTableEntries) {
-            try {
-                tableEntryMsgListBuilder.add(encodePiTableEntry(piTableEntry, browser));
-            } catch (P4InfoBrowser.NotFoundException e) {
-                throw new CodecException(e.getMessage());
-            }
-        }
-
-        return tableEntryMsgListBuilder.build();
-    }
-
-    /**
-     * Same as {@link #encode(List, PiPipeconf)} but encodes only one entry.
-     *
-     * @param piTableEntry table entry
-     * @param pipeconf     pipeconf
-     * @return encoded table entry message
-     * @throws CodecException                 if entry cannot be encoded
-     * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
-     */
-    static TableEntry encode(PiTableEntry piTableEntry, PiPipeconf pipeconf)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
-        }
-
-        return encodePiTableEntry(piTableEntry, browser);
-    }
-
-    /**
-     * Returns a collection of PI table entry objects, decoded from the given collection of P4Runtime table entry
-     * messages for the given pipeconf. If a table entry message cannot be decoded, it is skipped, hence the returned
-     * collection might have different size than the input one.
-     * <p>
-     * Please check the log for an explanation of any error that might have occurred.
-     *
-     * @param tableEntryMsgs P4Runtime table entry messages
-     * @param pipeconf       PI pipeconf
-     * @return collection of PI table entry objects
-     */
-    static List<PiTableEntry> decode(List<TableEntry> tableEntryMsgs, PiPipeconf pipeconf) {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            log.error("Unable to get a P4Info browser for pipeconf {}, skipping decoding of all table entries");
-            return Collections.emptyList();
-        }
-
-        ImmutableList.Builder<PiTableEntry> piTableEntryListBuilder = ImmutableList.builder();
-
-        for (TableEntry tableEntryMsg : tableEntryMsgs) {
-            try {
-                piTableEntryListBuilder.add(decodeTableEntryMsg(tableEntryMsg, browser));
-            } catch (P4InfoBrowser.NotFoundException | CodecException e) {
-                log.error("Unable to decode table entry message: {}", e.getMessage());
-            }
-        }
-
-        return piTableEntryListBuilder.build();
-    }
-
-    /**
-     * Same as {@link #decode(List, PiPipeconf)} but decodes only one entry.
-     *
-     * @param tableEntryMsg table entry message
-     * @param pipeconf      pipeconf
-     * @return decoded PI table entry
-     * @throws CodecException                 if message cannot be decoded
-     * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
-     */
-    static PiTableEntry decode(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
-        }
-        return decodeTableEntryMsg(tableEntryMsg, browser);
-    }
-
-    /**
-     * Returns a table entry protobuf message, encoded from the given table id and match key, for the given pipeconf.
-     * The returned table entry message can be only used to reference an existing entry, i.e. a read operation, and not
-     * a write one wince it misses other fields (action, priority, etc.).
-     *
-     * @param tableId  table identifier
-     * @param matchKey match key
-     * @param pipeconf pipeconf
-     * @return table entry message
-     * @throws CodecException                 if message cannot be encoded
-     * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
-     */
-    static TableEntry encode(PiTableId tableId, PiMatchKey matchKey, PiPipeconf pipeconf)
-            throws CodecException, P4InfoBrowser.NotFoundException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
-
-        P4InfoOuterClass.Table tableInfo = browser.tables().getByName(tableId.id());
-
-        // Table id.
-        tableEntryMsgBuilder.setTableId(tableInfo.getPreamble().getId());
-
-        // Field matches.
-        if (matchKey.equals(PiMatchKey.EMPTY)) {
-            tableEntryMsgBuilder.setIsDefaultAction(true);
-        } else {
-            for (PiFieldMatch piFieldMatch : matchKey.fieldMatches()) {
-                tableEntryMsgBuilder.addMatch(encodePiFieldMatch(piFieldMatch, tableInfo, browser));
-            }
-        }
-
-        return tableEntryMsgBuilder.build();
-    }
-
-    private static TableEntry encodePiTableEntry(PiTableEntry piTableEntry, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
-
-        P4InfoOuterClass.Table tableInfo = browser.tables().getByName(piTableEntry.table().id());
-
-        // Table id.
-        tableEntryMsgBuilder.setTableId(tableInfo.getPreamble().getId());
-
-        // Priority.
-        // FIXME: check on P4Runtime if/what is the default priority.
-        piTableEntry.priority().ifPresent(tableEntryMsgBuilder::setPriority);
-
-        // Controller metadata (cookie)
-        tableEntryMsgBuilder.setControllerMetadata(piTableEntry.cookie());
-
-        // Timeout.
-        if (piTableEntry.timeout().isPresent()) {
-            log.warn("Found PI table entry with timeout set, not supported in P4Runtime: {}", piTableEntry);
-        }
-
-        // Table action.
-        if (piTableEntry.action() != null) {
-            tableEntryMsgBuilder.setAction(encodePiTableAction(piTableEntry.action(), browser));
-        }
-
-        // Field matches.
-        if (piTableEntry.matchKey().equals(PiMatchKey.EMPTY)) {
-            tableEntryMsgBuilder.setIsDefaultAction(true);
-        } else {
-            for (PiFieldMatch piFieldMatch : piTableEntry.matchKey().fieldMatches()) {
-                tableEntryMsgBuilder.addMatch(encodePiFieldMatch(piFieldMatch, tableInfo, browser));
-            }
-        }
-
-        // Counter.
-        if (piTableEntry.counter() != null) {
-            tableEntryMsgBuilder.setCounterData(encodeCounter(piTableEntry.counter()));
-        }
-
-        return tableEntryMsgBuilder.build();
-    }
-
-    private static PiTableEntry decodeTableEntryMsg(TableEntry tableEntryMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        PiTableEntry.Builder piTableEntryBuilder = PiTableEntry.builder();
-
-        P4InfoOuterClass.Table tableInfo = browser.tables().getById(tableEntryMsg.getTableId());
-
-        // Table id.
-        piTableEntryBuilder.forTable(PiTableId.of(tableInfo.getPreamble().getName()));
-
-        // Priority.
-        if (tableEntryMsg.getPriority() > 0) {
-            piTableEntryBuilder.withPriority(tableEntryMsg.getPriority());
-        }
-
-        // Controller metadata (cookie)
-        piTableEntryBuilder.withCookie(tableEntryMsg.getControllerMetadata());
-
-        // Table action.
-        if (tableEntryMsg.hasAction()) {
-            piTableEntryBuilder.withAction(decodeTableActionMsg(tableEntryMsg.getAction(), browser));
-        }
-
-        // Timeout.
-        // FIXME: how to decode table entry messages with timeout, given that the timeout value is lost after encoding?
-
-        // Match key for field matches.
-        piTableEntryBuilder.withMatchKey(decodeFieldMatchMsgs(tableEntryMsg.getMatchList(), tableInfo, browser));
-
-        // Counter.
-        piTableEntryBuilder.withCounterCellData(decodeCounter(tableEntryMsg.getCounterData()));
-
-        return piTableEntryBuilder.build();
-    }
-
-    private static FieldMatch encodePiFieldMatch(PiFieldMatch piFieldMatch, P4InfoOuterClass.Table tableInfo,
-                                                 P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        FieldMatch.Builder fieldMatchMsgBuilder = FieldMatch.newBuilder();
-
-        // FIXME: check how field names for stacked headers are constructed in P4Runtime.
-        String fieldName = piFieldMatch.fieldId().id();
-        int tableId = tableInfo.getPreamble().getId();
-        P4InfoOuterClass.MatchField matchFieldInfo = browser.matchFields(tableId).getByName(fieldName);
-        String entityName = format("field match '%s' of table '%s'",
-                                   matchFieldInfo.getName(), tableInfo.getPreamble().getName());
-        int fieldId = matchFieldInfo.getId();
-        int fieldBitwidth = matchFieldInfo.getBitwidth();
-
-        fieldMatchMsgBuilder.setFieldId(fieldId);
-
-        switch (piFieldMatch.type()) {
-            case EXACT:
-                PiExactFieldMatch fieldMatch = (PiExactFieldMatch) piFieldMatch;
-                ByteString exactValue = ByteString.copyFrom(fieldMatch.value().asReadOnlyBuffer());
-                assertSize(VALUE_OF_PREFIX + entityName, exactValue, fieldBitwidth);
-                return fieldMatchMsgBuilder.setExact(
-                        FieldMatch.Exact
-                                .newBuilder()
-                                .setValue(exactValue)
-                                .build())
-                        .build();
-            case TERNARY:
-                PiTernaryFieldMatch ternaryMatch = (PiTernaryFieldMatch) piFieldMatch;
-                ByteString ternaryValue = ByteString.copyFrom(ternaryMatch.value().asReadOnlyBuffer());
-                ByteString ternaryMask = ByteString.copyFrom(ternaryMatch.mask().asReadOnlyBuffer());
-                assertSize(VALUE_OF_PREFIX + entityName, ternaryValue, fieldBitwidth);
-                assertSize(MASK_OF_PREFIX + entityName, ternaryMask, fieldBitwidth);
-                return fieldMatchMsgBuilder.setTernary(
-                        FieldMatch.Ternary
-                                .newBuilder()
-                                .setValue(ternaryValue)
-                                .setMask(ternaryMask)
-                                .build())
-                        .build();
-            case LPM:
-                PiLpmFieldMatch lpmMatch = (PiLpmFieldMatch) piFieldMatch;
-                ByteString lpmValue = ByteString.copyFrom(lpmMatch.value().asReadOnlyBuffer());
-                int lpmPrefixLen = lpmMatch.prefixLength();
-                assertSize(VALUE_OF_PREFIX + entityName, lpmValue, fieldBitwidth);
-                assertPrefixLen(entityName, lpmPrefixLen, fieldBitwidth);
-                return fieldMatchMsgBuilder.setLpm(
-                        FieldMatch.LPM.newBuilder()
-                                .setValue(lpmValue)
-                                .setPrefixLen(lpmPrefixLen)
-                                .build())
-                        .build();
-            case RANGE:
-                PiRangeFieldMatch rangeMatch = (PiRangeFieldMatch) piFieldMatch;
-                ByteString rangeHighValue = ByteString.copyFrom(rangeMatch.highValue().asReadOnlyBuffer());
-                ByteString rangeLowValue = ByteString.copyFrom(rangeMatch.lowValue().asReadOnlyBuffer());
-                assertSize(HIGH_RANGE_VALUE_OF_PREFIX + entityName, rangeHighValue, fieldBitwidth);
-                assertSize(LOW_RANGE_VALUE_OF_PREFIX + entityName, rangeLowValue, fieldBitwidth);
-                return fieldMatchMsgBuilder.setRange(
-                        FieldMatch.Range.newBuilder()
-                                .setHigh(rangeHighValue)
-                                .setLow(rangeLowValue)
-                                .build())
-                        .build();
-            default:
-                throw new CodecException(format(
-                        "Building of match type %s not implemented", piFieldMatch.type()));
-        }
-    }
-
-    /**
-     * Returns a PI match key, decoded from the given table entry protobuf message, for the given pipeconf.
-     *
-     * @param tableEntryMsg table entry message
-     * @param pipeconf      pipeconf
-     * @return PI match key
-     * @throws CodecException                 if message cannot be decoded
-     * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
-     */
-    static PiMatchKey decodeMatchKey(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        P4InfoOuterClass.Table tableInfo = browser.tables().getById(tableEntryMsg.getTableId());
-        if (tableEntryMsg.getMatchCount() == 0) {
-            return PiMatchKey.EMPTY;
-        } else {
-            return decodeFieldMatchMsgs(tableEntryMsg.getMatchList(), tableInfo, browser);
-        }
-    }
-
-    private static PiMatchKey decodeFieldMatchMsgs(List<FieldMatch> fieldMatchs, P4InfoOuterClass.Table tableInfo,
-                                                   P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-        // Match key for field matches.
-        PiMatchKey.Builder piMatchKeyBuilder = PiMatchKey.builder();
-        for (FieldMatch fieldMatchMsg : fieldMatchs) {
-            piMatchKeyBuilder.addFieldMatch(decodeFieldMatchMsg(fieldMatchMsg, tableInfo, browser));
-        }
-        return piMatchKeyBuilder.build();
-    }
-
-    private static PiFieldMatch decodeFieldMatchMsg(FieldMatch fieldMatchMsg, P4InfoOuterClass.Table tableInfo,
-                                                    P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        int tableId = tableInfo.getPreamble().getId();
-        String fieldMatchName = browser.matchFields(tableId).getById(fieldMatchMsg.getFieldId()).getName();
-        PiMatchFieldId headerFieldId = PiMatchFieldId.of(fieldMatchName);
-
-        FieldMatch.FieldMatchTypeCase typeCase = fieldMatchMsg.getFieldMatchTypeCase();
-
-        switch (typeCase) {
-            case EXACT:
-                FieldMatch.Exact exactFieldMatch = fieldMatchMsg.getExact();
-                ImmutableByteSequence exactValue = copyFrom(exactFieldMatch.getValue().asReadOnlyByteBuffer());
-                return new PiExactFieldMatch(headerFieldId, exactValue);
-            case TERNARY:
-                FieldMatch.Ternary ternaryFieldMatch = fieldMatchMsg.getTernary();
-                ImmutableByteSequence ternaryValue = copyFrom(ternaryFieldMatch.getValue().asReadOnlyByteBuffer());
-                ImmutableByteSequence ternaryMask = copyFrom(ternaryFieldMatch.getMask().asReadOnlyByteBuffer());
-                return new PiTernaryFieldMatch(headerFieldId, ternaryValue, ternaryMask);
-            case LPM:
-                FieldMatch.LPM lpmFieldMatch = fieldMatchMsg.getLpm();
-                ImmutableByteSequence lpmValue = copyFrom(lpmFieldMatch.getValue().asReadOnlyByteBuffer());
-                int lpmPrefixLen = lpmFieldMatch.getPrefixLen();
-                return new PiLpmFieldMatch(headerFieldId, lpmValue, lpmPrefixLen);
-            case RANGE:
-                FieldMatch.Range rangeFieldMatch = fieldMatchMsg.getRange();
-                ImmutableByteSequence rangeHighValue = copyFrom(rangeFieldMatch.getHigh().asReadOnlyByteBuffer());
-                ImmutableByteSequence rangeLowValue = copyFrom(rangeFieldMatch.getLow().asReadOnlyByteBuffer());
-                return new PiRangeFieldMatch(headerFieldId, rangeLowValue, rangeHighValue);
-            default:
-                throw new CodecException(format(
-                        "Decoding of field match type '%s' not implemented", typeCase.name()));
-        }
-    }
-
-    static TableAction encodePiTableAction(PiTableAction piTableAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-        checkNotNull(piTableAction, "Cannot encode null PiTableAction");
-        TableAction.Builder tableActionMsgBuilder = TableAction.newBuilder();
-
-        switch (piTableAction.type()) {
-            case ACTION:
-                PiAction piAction = (PiAction) piTableAction;
-                Action theAction = encodePiAction(piAction, browser);
-                tableActionMsgBuilder.setAction(theAction);
-                break;
-            case ACTION_PROFILE_GROUP_ID:
-                PiActionProfileGroupId actionGroupId = (PiActionProfileGroupId) piTableAction;
-                tableActionMsgBuilder.setActionProfileGroupId(actionGroupId.id());
-                break;
-            case ACTION_PROFILE_MEMBER_ID:
-                PiActionProfileMemberId actionProfileMemberId = (PiActionProfileMemberId) piTableAction;
-                tableActionMsgBuilder.setActionProfileMemberId(actionProfileMemberId.id());
-                break;
-            default:
-                throw new CodecException(
-                        format("Building of table action type %s not implemented", piTableAction.type()));
-        }
-
-        return tableActionMsgBuilder.build();
-    }
-
-    static PiTableAction decodeTableActionMsg(TableAction tableActionMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-        TableAction.TypeCase typeCase = tableActionMsg.getTypeCase();
-        switch (typeCase) {
-            case ACTION:
-                Action actionMsg = tableActionMsg.getAction();
-                return decodeActionMsg(actionMsg, browser);
-            case ACTION_PROFILE_GROUP_ID:
-                return PiActionProfileGroupId.of(tableActionMsg.getActionProfileGroupId());
-            case ACTION_PROFILE_MEMBER_ID:
-                return PiActionProfileMemberId.of(tableActionMsg.getActionProfileMemberId());
-            default:
-                throw new CodecException(
-                        format("Decoding of table action type %s not implemented", typeCase.name()));
-        }
-    }
-
-    static Action encodePiAction(PiAction piAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, CodecException {
-
-        int actionId = browser.actions().getByName(piAction.id().toString()).getPreamble().getId();
-
-        Action.Builder actionMsgBuilder =
-                Action.newBuilder().setActionId(actionId);
-
-        for (PiActionParam p : piAction.parameters()) {
-            P4InfoOuterClass.Action.Param paramInfo = browser.actionParams(actionId).getByName(p.id().toString());
-            ByteString paramValue = ByteString.copyFrom(p.value().asReadOnlyBuffer());
-            assertSize(format("param '%s' of action '%s'", p.id(), piAction.id()),
-                       paramValue, paramInfo.getBitwidth());
-            actionMsgBuilder.addParams(Action.Param.newBuilder()
-                                               .setParamId(paramInfo.getId())
-                                               .setValue(paramValue)
-                                               .build());
-        }
-
-        return actionMsgBuilder.build();
-    }
-
-    static PiAction decodeActionMsg(Action action, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException {
-        P4InfoBrowser.EntityBrowser<P4InfoOuterClass.Action.Param> paramInfo =
-                browser.actionParams(action.getActionId());
-        String actionName = browser.actions()
-                .getById(action.getActionId())
-                .getPreamble().getName();
-        PiActionId id = PiActionId.of(actionName);
-        List<PiActionParam> params = Lists.newArrayList();
-
-        for (Action.Param p : action.getParamsList()) {
-            String paramName = paramInfo.getById(p.getParamId()).getName();
-            ImmutableByteSequence value = ImmutableByteSequence.copyFrom(p.getValue().toByteArray());
-            params.add(new PiActionParam(PiActionParamId.of(paramName), value));
-        }
-        return PiAction.builder().withId(id).withParameters(params).build();
-    }
-
-    static CounterData encodeCounter(PiCounterCellData piCounterCellData) {
-        return CounterData.newBuilder().setPacketCount(piCounterCellData.packets())
-                .setByteCount(piCounterCellData.bytes()).build();
-    }
-
-    static PiCounterCellData decodeCounter(CounterData counterData) {
-        return new PiCounterCellData(counterData.getPacketCount(), counterData.getByteCount());
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/P4RuntimeClientImpl.java
new file mode 100644
index 0000000..353d44e
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/P4RuntimeClientImpl.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import io.grpc.ManagedChannel;
+import io.grpc.StatusRuntimeException;
+import org.onosproject.grpc.ctl.AbstractGrpcClient;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.net.pi.service.PiPipeconfService;
+import org.onosproject.p4runtime.api.P4RuntimeClient;
+import org.onosproject.p4runtime.api.P4RuntimeClientKey;
+import org.onosproject.p4runtime.api.P4RuntimeEvent;
+import org.onosproject.p4runtime.ctl.controller.BaseEventSubject;
+import org.onosproject.p4runtime.ctl.controller.ChannelEvent;
+import org.onosproject.p4runtime.ctl.controller.P4RuntimeControllerImpl;
+import p4.v1.P4RuntimeGrpc;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Implementation of P4RuntimeClient.
+ */
+public final class P4RuntimeClientImpl
+        extends AbstractGrpcClient implements P4RuntimeClient {
+
+    // TODO: consider making timeouts configurable per-device via netcfg
+    /**
+     * Timeout in seconds for short/fast RPCs.
+     */
+    static final int SHORT_TIMEOUT_SECONDS = 10;
+    /**
+     * Timeout in seconds for RPCs that involve transfer of potentially large
+     * amount of data. This shoulld be long enough to allow for network delay
+     * (e.g. to transfer large pipeline binaries over slow network).
+     */
+    static final int LONG_TIMEOUT_SECONDS = 60;
+
+    private final long p4DeviceId;
+    private final ManagedChannel channel;
+    private final P4RuntimeControllerImpl controller;
+    private final StreamClientImpl streamClient;
+    private final PipelineConfigClientImpl pipelineConfigClient;
+
+    /**
+     * Instantiates a new client with the given arguments.
+     *
+     * @param clientKey       client key
+     * @param channel         gRPC managed channel
+     * @param controller      P$Runtime controller instance
+     * @param pipeconfService pipeconf service instance
+     */
+    public P4RuntimeClientImpl(P4RuntimeClientKey clientKey,
+                               ManagedChannel channel,
+                               P4RuntimeControllerImpl controller,
+                               PiPipeconfService pipeconfService) {
+        super(clientKey);
+        checkNotNull(channel);
+        checkNotNull(controller);
+        checkNotNull(pipeconfService);
+
+        this.p4DeviceId = clientKey.p4DeviceId();
+        this.channel = channel;
+        this.controller = controller;
+        this.streamClient = new StreamClientImpl(
+                pipeconfService, this, controller);
+        this.pipelineConfigClient = new PipelineConfigClientImpl(this);
+    }
+
+    @Override
+    protected Void doShutdown() {
+        streamClient.closeSession();
+        return super.doShutdown();
+    }
+
+    @Override
+    public CompletableFuture<Boolean> setPipelineConfig(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+        return pipelineConfigClient.setPipelineConfig(pipeconf, deviceData);
+    }
+
+    @Override
+    public CompletableFuture<Boolean> isPipelineConfigSet(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+        return pipelineConfigClient.isPipelineConfigSet(pipeconf, deviceData);
+    }
+
+    @Override
+    public ReadRequest read(PiPipeconf pipeconf) {
+        return new ReadRequestImpl(this, pipeconf);
+    }
+
+    @Override
+    public void openSession() {
+        streamClient.openSession();
+    }
+
+    @Override
+    public boolean isSessionOpen() {
+        return streamClient.isSessionOpen();
+    }
+
+    @Override
+    public void closeSession() {
+        streamClient.closeSession();
+    }
+
+    @Override
+    public void runForMastership() {
+        streamClient.runForMastership();
+    }
+
+    @Override
+    public boolean isMaster() {
+        return streamClient.isMaster();
+    }
+
+    @Override
+    public void packetOut(PiPacketOperation packet, PiPipeconf pipeconf) {
+        streamClient.packetOut(packet, pipeconf);
+    }
+
+    @Override
+    public WriteRequest write(PiPipeconf pipeconf) {
+        return new WriteRequestImpl(this, pipeconf);
+    }
+
+    /**
+     * Returns the P4Runtime-internal device ID associated with this client.
+     *
+     * @return P4Runtime-internal device ID
+     */
+    long p4DeviceId() {
+        return this.p4DeviceId;
+    }
+
+    /**
+     * Returns the ONOS device ID associated with this client.
+     *
+     * @return ONOS device ID
+     */
+    DeviceId deviceId() {
+        return this.deviceId;
+    }
+
+    /**
+     * Returns the election ID last used in a MasterArbitrationUpdate message
+     * sent by the client to the server. No guarantees are given that this is
+     * the current election ID associated to the session, nor that the server
+     * has acknowledged this value as valid.
+     *
+     * @return election ID uint128 protobuf message
+     */
+    P4RuntimeOuterClass.Uint128 lastUsedElectionId() {
+        return streamClient.lastUsedElectionId();
+    }
+
+    /**
+     * Forces execution of an RPC in a cancellable context with the given
+     * timeout (in seconds).
+     *
+     * @param stubConsumer P4Runtime stub consumer
+     * @param timeout      timeout in seconds
+     */
+    void execRpc(Consumer<P4RuntimeGrpc.P4RuntimeStub> stubConsumer, int timeout) {
+        if (log.isTraceEnabled()) {
+            log.trace("Executing RPC with timeout {} seconds (context deadline {})...",
+                      timeout, context().getDeadline());
+        }
+        runInCancellableContext(() -> stubConsumer.accept(
+                P4RuntimeGrpc.newStub(channel)
+                        .withDeadlineAfter(timeout, TimeUnit.SECONDS)));
+    }
+
+    /**
+     * Forces execution of an RPC in a cancellable context with no timeout.
+     *
+     * @param stubConsumer P4Runtime stub consumer
+     */
+    void execRpcNoTimeout(Consumer<P4RuntimeGrpc.P4RuntimeStub> stubConsumer) {
+        if (log.isTraceEnabled()) {
+            log.trace("Executing RPC with no timeout (context deadline {})...",
+                      context().getDeadline());
+        }
+        runInCancellableContext(() -> stubConsumer.accept(
+                P4RuntimeGrpc.newStub(channel)));
+    }
+
+    /**
+     * Logs the error and checks it for any condition that might be of interest
+     * for the controller.
+     *
+     * @param throwable     throwable
+     * @param opDescription operation description for logging
+     */
+    void handleRpcError(Throwable throwable, String opDescription) {
+        if (throwable instanceof StatusRuntimeException) {
+            final StatusRuntimeException sre = (StatusRuntimeException) throwable;
+            checkGrpcException(sre);
+            final String logMsg;
+            if (sre.getCause() == null) {
+                logMsg = sre.getMessage();
+            } else {
+                logMsg = format("%s (%s)", sre.getMessage(), sre.getCause().toString());
+            }
+            log.warn("Error while performing {} on {}: {}",
+                     opDescription, deviceId, logMsg);
+            log.debug("", throwable);
+            return;
+        }
+        log.error(format("Exception while performing %s on %s",
+                         opDescription, deviceId), throwable);
+    }
+
+    private void checkGrpcException(StatusRuntimeException sre) {
+        switch (sre.getStatus().getCode()) {
+            case PERMISSION_DENIED:
+                // Notify upper layers that this node is not master.
+                controller.postEvent(new P4RuntimeEvent(
+                        P4RuntimeEvent.Type.PERMISSION_DENIED,
+                        new BaseEventSubject(deviceId)));
+                break;
+            case UNAVAILABLE:
+                // Channel might be closed.
+                controller.postEvent(new P4RuntimeEvent(
+                        P4RuntimeEvent.Type.CHANNEL_EVENT,
+                        new ChannelEvent(deviceId, ChannelEvent.Type.ERROR)));
+                break;
+            default:
+                break;
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/PipelineConfigClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/PipelineConfigClientImpl.java
new file mode 100644
index 0000000..c023f26
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/PipelineConfigClientImpl.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.TextFormat;
+import io.grpc.stub.StreamObserver;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.p4runtime.api.P4RuntimePipelineConfigClient;
+import org.onosproject.p4runtime.ctl.utils.PipeconfHelper;
+import org.slf4j.Logger;
+import p4.config.v1.P4InfoOuterClass;
+import p4.tmp.P4Config;
+import p4.v1.P4RuntimeOuterClass.ForwardingPipelineConfig;
+import p4.v1.P4RuntimeOuterClass.GetForwardingPipelineConfigRequest;
+import p4.v1.P4RuntimeOuterClass.GetForwardingPipelineConfigResponse;
+import p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest;
+import p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigResponse;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.onosproject.p4runtime.ctl.client.P4RuntimeClientImpl.LONG_TIMEOUT_SECONDS;
+import static org.slf4j.LoggerFactory.getLogger;
+import static p4.v1.P4RuntimeOuterClass.GetForwardingPipelineConfigRequest.ResponseType.COOKIE_ONLY;
+import static p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest.Action.VERIFY_AND_COMMIT;
+
+/**
+ * Implementation of P4RuntimePipelineConfigClient. Handles pipeline
+ * config-related RPCs.
+ */
+final class PipelineConfigClientImpl implements P4RuntimePipelineConfigClient {
+
+    private static final Logger log = getLogger(PipelineConfigClientImpl.class);
+
+    private static final SetForwardingPipelineConfigResponse DEFAULT_SET_RESPONSE =
+            SetForwardingPipelineConfigResponse.getDefaultInstance();
+
+    private final P4RuntimeClientImpl client;
+
+    PipelineConfigClientImpl(P4RuntimeClientImpl client) {
+        this.client = client;
+    }
+
+    @Override
+    public CompletableFuture<Boolean> setPipelineConfig(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+
+        log.info("Setting pipeline config for {} to {}...",
+                 client.deviceId(), pipeconf.id());
+
+        checkNotNull(deviceData, "deviceData cannot be null");
+
+        final ForwardingPipelineConfig pipelineConfigMsg =
+                buildForwardingPipelineConfigMsg(pipeconf, deviceData);
+        if (pipelineConfigMsg == null) {
+            // Error logged in buildForwardingPipelineConfigMsg()
+            return completedFuture(false);
+        }
+
+        final SetForwardingPipelineConfigRequest requestMsg =
+                SetForwardingPipelineConfigRequest
+                        .newBuilder()
+                        .setDeviceId(client.p4DeviceId())
+                        .setElectionId(client.lastUsedElectionId())
+                        .setAction(VERIFY_AND_COMMIT)
+                        .setConfig(pipelineConfigMsg)
+                        .build();
+
+        final CompletableFuture<Boolean> future = new CompletableFuture<>();
+        final StreamObserver<SetForwardingPipelineConfigResponse> responseObserver =
+                new StreamObserver<SetForwardingPipelineConfigResponse>() {
+                    @Override
+                    public void onNext(SetForwardingPipelineConfigResponse value) {
+                        if (!DEFAULT_SET_RESPONSE.equals(value)) {
+                            log.warn("Received invalid SetForwardingPipelineConfigResponse " +
+                                             " from {} [{}]",
+                                     client.deviceId(),
+                                     TextFormat.shortDebugString(value));
+                            future.complete(false);
+                        }
+                        // All good, pipeline is set.
+                        future.complete(true);
+                    }
+                    @Override
+                    public void onError(Throwable t) {
+                        client.handleRpcError(t, "SET-pipeline-config");
+                        future.complete(false);
+                    }
+                    @Override
+                    public void onCompleted() {
+                        // Ignore, unary call.
+                    }
+                };
+
+        client.execRpc(
+                s -> s.setForwardingPipelineConfig(requestMsg, responseObserver),
+                LONG_TIMEOUT_SECONDS);
+
+        return future;
+    }
+
+    private ForwardingPipelineConfig buildForwardingPipelineConfigMsg(
+            PiPipeconf pipeconf, ByteBuffer deviceData) {
+
+        final P4InfoOuterClass.P4Info p4Info = PipeconfHelper.getP4Info(pipeconf);
+        if (p4Info == null) {
+            // Problem logged by PipeconfHelper.
+            return null;
+        }
+        final ForwardingPipelineConfig.Cookie cookieMsg =
+                ForwardingPipelineConfig.Cookie
+                        .newBuilder()
+                        .setCookie(pipeconf.fingerprint())
+                        .build();
+        // FIXME: This is specific to PI P4Runtime implementation and should be
+        //  moved to driver.
+        final P4Config.P4DeviceConfig p4DeviceConfigMsg = P4Config.P4DeviceConfig
+                .newBuilder()
+                .setExtras(P4Config.P4DeviceConfig.Extras.getDefaultInstance())
+                .setReassign(true)
+                .setDeviceData(ByteString.copyFrom(deviceData))
+                .build();
+        return ForwardingPipelineConfig
+                .newBuilder()
+                .setP4Info(p4Info)
+                .setP4DeviceConfig(p4DeviceConfigMsg.toByteString())
+                .setCookie(cookieMsg)
+                .build();
+    }
+
+
+    @Override
+    public CompletableFuture<Boolean> isPipelineConfigSet(
+            PiPipeconf pipeconf, ByteBuffer expectedDeviceData) {
+        return getPipelineCookieFromServer()
+                .thenApply(cfgFromDevice -> comparePipelineConfig(
+                        pipeconf, expectedDeviceData, cfgFromDevice));
+    }
+
+    private boolean comparePipelineConfig(
+            PiPipeconf pipeconf, ByteBuffer expectedDeviceData,
+            ForwardingPipelineConfig cfgFromDevice) {
+        if (cfgFromDevice == null) {
+            return false;
+        }
+
+        final ForwardingPipelineConfig expectedCfg = buildForwardingPipelineConfigMsg(
+                pipeconf, expectedDeviceData);
+        if (expectedCfg == null) {
+            return false;
+        }
+
+        if (cfgFromDevice.hasCookie()) {
+            return cfgFromDevice.getCookie().getCookie() == pipeconf.fingerprint();
+        }
+
+        // No cookie.
+        log.warn("{} returned GetForwardingPipelineConfigResponse " +
+                         "with 'cookie' field unset. " +
+                         "Will try by comparing 'device_data' and 'p4_info'...",
+                 client.deviceId());
+
+        if (cfgFromDevice.getP4DeviceConfig().isEmpty()
+                && !expectedCfg.getP4DeviceConfig().isEmpty()) {
+            // Don't bother with a warn or error since we don't really allow
+            // updating the P4 blob to a different one without changing the
+            // P4Info. I.e, comparing just the P4Info should be enough for us.
+            log.debug("{} returned GetForwardingPipelineConfigResponse " +
+                              "with empty 'p4_device_config' field, " +
+                              "equality will be based only on P4Info",
+                      client.deviceId());
+            return cfgFromDevice.getP4Info().equals(expectedCfg.getP4Info());
+        }
+
+        return cfgFromDevice.getP4DeviceConfig()
+                .equals(expectedCfg.getP4DeviceConfig())
+                && cfgFromDevice.getP4Info()
+                .equals(expectedCfg.getP4Info());
+    }
+
+    private CompletableFuture<ForwardingPipelineConfig> getPipelineCookieFromServer() {
+        final GetForwardingPipelineConfigRequest request =
+                GetForwardingPipelineConfigRequest
+                        .newBuilder()
+                        .setDeviceId(client.p4DeviceId())
+                        .setResponseType(COOKIE_ONLY)
+                        .build();
+        final CompletableFuture<ForwardingPipelineConfig> future = new CompletableFuture<>();
+        final StreamObserver<GetForwardingPipelineConfigResponse> responseObserver =
+                new StreamObserver<GetForwardingPipelineConfigResponse>() {
+                    @Override
+                    public void onNext(GetForwardingPipelineConfigResponse value) {
+                        if (value.hasConfig()) {
+                            future.complete(value.getConfig());
+                        } else {
+                            log.warn("{} returned {} with 'config' field unset",
+                                     client.deviceId(), value.getClass().getSimpleName());
+                        }
+                        future.complete(null);
+                    }
+
+                    @Override
+                    public void onError(Throwable t) {
+                        client.handleRpcError(t, "GET-pipeline-config");
+                        future.complete(null);
+                    }
+
+                    @Override
+                    public void onCompleted() {
+                        // Ignore, unary call.
+                    }
+                };
+        // Use long timeout as the device might return the full P4 blob
+        // (e.g. server does not support cookie), over a slow network.
+        client.execRpc(
+                s -> s.getForwardingPipelineConfig(request, responseObserver),
+                LONG_TIMEOUT_SECONDS);
+        return future;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadRequestImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadRequestImpl.java
new file mode 100644
index 0000000..c85c7e8
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadRequestImpl.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.common.util.concurrent.Futures;
+import io.grpc.stub.StreamObserver;
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiCounterId;
+import org.onosproject.net.pi.model.PiMeterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.api.P4RuntimeReadClient;
+import org.onosproject.p4runtime.ctl.codec.CodecException;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import org.onosproject.p4runtime.ctl.utils.PipeconfHelper;
+import org.slf4j.Logger;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.concurrent.CompletableFuture;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.onosproject.p4runtime.ctl.client.P4RuntimeClientImpl.SHORT_TIMEOUT_SECONDS;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Handles the creation of P4Runtime ReadRequest and execution of the Read RPC
+ * on the server.
+ */
+public final class ReadRequestImpl implements P4RuntimeReadClient.ReadRequest {
+
+    private static final Logger log = getLogger(ReadRequestImpl.class);
+
+    private final P4RuntimeClientImpl client;
+    private final PiPipeconf pipeconf;
+    private final P4RuntimeOuterClass.ReadRequest.Builder requestMsg;
+
+    ReadRequestImpl(P4RuntimeClientImpl client, PiPipeconf pipeconf) {
+        this.client = client;
+        this.pipeconf = pipeconf;
+        this.requestMsg = P4RuntimeOuterClass.ReadRequest.newBuilder()
+                .setDeviceId(client.p4DeviceId());
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest handles(Iterable<? extends PiHandle> handles) {
+        checkNotNull(handles);
+        handles.forEach(this::handle);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest tableEntries(Iterable<PiTableId> tableIds) {
+        checkNotNull(tableIds);
+        tableIds.forEach(this::tableEntries);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest defaultTableEntry(Iterable<PiTableId> tableIds) {
+        checkNotNull(tableIds);
+        tableIds.forEach(this::defaultTableEntry);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest actionProfileGroups(Iterable<PiActionProfileId> actionProfileIds) {
+        checkNotNull(actionProfileIds);
+        actionProfileIds.forEach(this::actionProfileGroups);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest actionProfileMembers(Iterable<PiActionProfileId> actionProfileIds) {
+        checkNotNull(actionProfileIds);
+        actionProfileIds.forEach(this::actionProfileMembers);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest counterCells(Iterable<PiCounterId> counterIds) {
+        checkNotNull(counterIds);
+        counterIds.forEach(this::counterCells);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest directCounterCells(Iterable<PiTableId> tableIds) {
+        checkNotNull(tableIds);
+        tableIds.forEach(this::directCounterCells);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest meterCells(Iterable<PiMeterId> meterIds) {
+        checkNotNull(meterIds);
+        meterIds.forEach(this::meterCells);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest directMeterCells(Iterable<PiTableId> tableIds) {
+        checkNotNull(tableIds);
+        tableIds.forEach(this::directMeterCells);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest handle(PiHandle handle) {
+        checkNotNull(handle);
+        try {
+            requestMsg.addEntities(CODECS.handle().encode(handle, null, pipeconf));
+        } catch (CodecException e) {
+            log.warn("Unable to read {} from {}: {} [{}]",
+                     handle.entityType(), client.deviceId(), e.getMessage(), handle);
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest tableEntries(PiTableId tableId) {
+        try {
+            doTableEntry(tableId, false);
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read entries for table '{}' from {}: {}",
+                     tableId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest defaultTableEntry(PiTableId tableId) {
+        try {
+            doTableEntry(tableId, true);
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read default entry for table '{}' from {}: {}",
+                     tableId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest actionProfileGroups(PiActionProfileId actionProfileId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setActionProfileGroup(
+                                    P4RuntimeOuterClass.ActionProfileGroup.newBuilder()
+                                            .setActionProfileId(
+                                                    p4ActionProfileId(actionProfileId))
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read groups for action profile '{}' from {}: {}",
+                     actionProfileId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest actionProfileMembers(PiActionProfileId actionProfileId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setActionProfileMember(
+                                    P4RuntimeOuterClass.ActionProfileMember.newBuilder()
+                                            .setActionProfileId(
+                                                    p4ActionProfileId(actionProfileId))
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read members for action profile '{}' from {}: {}",
+                     actionProfileId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest counterCells(PiCounterId counterId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setCounterEntry(
+                                    P4RuntimeOuterClass.CounterEntry.newBuilder()
+                                            .setCounterId(p4CounterId(counterId))
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read cells for counter '{}' from {}: {}",
+                     counterId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest meterCells(PiMeterId meterId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setMeterEntry(
+                                    P4RuntimeOuterClass.MeterEntry.newBuilder()
+                                            .setMeterId(p4MeterId(meterId))
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read cells for meter '{}' from {}: {}",
+                     meterId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest directCounterCells(PiTableId tableId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setDirectCounterEntry(
+                                    P4RuntimeOuterClass.DirectCounterEntry.newBuilder()
+                                            .setTableEntry(
+                                                    P4RuntimeOuterClass.TableEntry
+                                                            .newBuilder()
+                                                            .setTableId(p4TableId(tableId))
+                                                            .build())
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read direct counter cells for table '{}' from {}: {}",
+                     tableId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadRequest directMeterCells(PiTableId tableId) {
+        try {
+            requestMsg.addEntities(
+                    P4RuntimeOuterClass.Entity.newBuilder()
+                            .setDirectMeterEntry(
+                                    P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
+                                            .setTableEntry(
+                                                    P4RuntimeOuterClass.TableEntry
+                                                            .newBuilder()
+                                                            .setTableId(p4TableId(tableId))
+                                                            .build())
+                                            .build())
+                            .build());
+        } catch (InternalRequestException e) {
+            log.warn("Unable to read direct meter cells for table '{}' from {}: {}",
+                     tableId, client.deviceId(), e.getMessage());
+        }
+        return this;
+    }
+
+    private void doTableEntry(PiTableId piTableId, boolean defaultEntries)
+            throws InternalRequestException {
+        checkNotNull(piTableId);
+        final P4RuntimeOuterClass.Entity entityMsg = P4RuntimeOuterClass.Entity
+                .newBuilder()
+                .setTableEntry(
+                        P4RuntimeOuterClass.TableEntry.newBuilder()
+                                .setTableId(p4TableId(piTableId))
+                                .setIsDefaultAction(defaultEntries)
+                                .setCounterData(P4RuntimeOuterClass.CounterData
+                                                        .getDefaultInstance())
+                                .build())
+                .build();
+        requestMsg.addEntities(entityMsg);
+    }
+
+    @Override
+    public CompletableFuture<P4RuntimeReadClient.ReadResponse> submit() {
+        final P4RuntimeOuterClass.ReadRequest readRequest = requestMsg.build();
+        log.debug("Sending read request to {} for {} entities...",
+                  client.deviceId(), readRequest.getEntitiesCount());
+        if (readRequest.getEntitiesCount() == 0) {
+            // No need to ask the server.
+            return completedFuture(ReadResponseImpl.EMPTY);
+        }
+        final CompletableFuture<P4RuntimeReadClient.ReadResponse> future =
+                new CompletableFuture<>();
+        // Instantiate response builder and let stream observer populate it.
+        final ReadResponseImpl.Builder responseBuilder =
+                ReadResponseImpl.builder(client.deviceId(), pipeconf);
+        final StreamObserver<P4RuntimeOuterClass.ReadResponse> observer =
+                new StreamObserver<P4RuntimeOuterClass.ReadResponse>() {
+                    @Override
+                    public void onNext(P4RuntimeOuterClass.ReadResponse value) {
+                        log.debug("Received read response from {} with {} entities...",
+                                  client.deviceId(), value.getEntitiesCount());
+                        value.getEntitiesList().forEach(responseBuilder::addEntity);
+                    }
+                    @Override
+                    public void onError(Throwable t) {
+                        client.handleRpcError(t, "READ");
+                        // TODO: implement parsing of trailer errors
+                        future.complete(responseBuilder.fail(t));
+                    }
+                    @Override
+                    public void onCompleted() {
+                        future.complete(responseBuilder.build());
+                    }
+                };
+        client.execRpc(s -> s.read(readRequest, observer), SHORT_TIMEOUT_SECONDS);
+        return future;
+    }
+
+    @Override
+    public P4RuntimeReadClient.ReadResponse submitSync() {
+        return Futures.getUnchecked(submit());
+    }
+
+    private int p4TableId(PiTableId piTableId) throws InternalRequestException {
+        try {
+            return getBrowser().tables().getByName(piTableId.id())
+                    .getPreamble().getId();
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new InternalRequestException(e.getMessage());
+        }
+    }
+
+    private int p4ActionProfileId(PiActionProfileId piActionProfileId)
+            throws InternalRequestException {
+        try {
+            return getBrowser().actionProfiles().getByName(piActionProfileId.id())
+                    .getPreamble().getId();
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new InternalRequestException(e.getMessage());
+        }
+    }
+
+    private int p4CounterId(PiCounterId counterId)
+            throws InternalRequestException {
+        try {
+            return getBrowser().counters().getByName(counterId.id())
+                    .getPreamble().getId();
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new InternalRequestException(e.getMessage());
+        }
+    }
+
+    private int p4MeterId(PiMeterId meterId)
+            throws InternalRequestException {
+        try {
+            return getBrowser().meters().getByName(meterId.id())
+                    .getPreamble().getId();
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new InternalRequestException(e.getMessage());
+        }
+    }
+
+    private P4InfoBrowser getBrowser() throws InternalRequestException {
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new InternalRequestException(
+                    "Unable to get a P4Info browser for pipeconf " + pipeconf.id());
+        }
+        return browser;
+    }
+
+    /**
+     * Internal exception to signal that something went wrong when populating
+     * the request.
+     */
+    private final class InternalRequestException extends Exception {
+
+        private InternalRequestException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadResponseImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadResponseImpl.java
new file mode 100644
index 0000000..5e57797
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/ReadResponseImpl.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.p4runtime.api.P4RuntimeReadClient;
+import org.onosproject.p4runtime.ctl.codec.CodecException;
+import org.slf4j.Logger;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.Collection;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Handles creation of ReadResponse by parsing Read RPC server responses.
+ */
+public final class ReadResponseImpl implements P4RuntimeReadClient.ReadResponse {
+
+    private static final Logger log = getLogger(ReadResponseImpl.class);
+
+    public static final ReadResponseImpl EMPTY = new ReadResponseImpl(
+            true, ImmutableList.of(), ImmutableListMultimap.of(), null, null);
+
+    private final boolean success;
+    private final ImmutableList<PiEntity> entities;
+    private final ImmutableListMultimap<Class<? extends PiEntity>, PiEntity> typeToEntities;
+    private final String explanation;
+    private final Throwable throwable;
+
+    private ReadResponseImpl(
+            boolean success, ImmutableList<PiEntity> entities,
+            ImmutableListMultimap<Class<? extends PiEntity>, PiEntity> typeToEntities,
+            String explanation, Throwable throwable) {
+        this.success = success;
+        this.entities = entities;
+        this.typeToEntities = typeToEntities;
+        this.explanation = explanation;
+        this.throwable = throwable;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return success;
+    }
+
+    @Override
+    public Collection<PiEntity> all() {
+        return entities;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <E extends PiEntity> Collection<E> all(Class<E> clazz) {
+        return (ImmutableList<E>) typeToEntities.get(clazz);
+    }
+
+    @Override
+    public String explanation() {
+        return explanation;
+    }
+
+    @Override
+    public Throwable throwable() {
+        return throwable;
+    }
+
+    static Builder builder(DeviceId deviceId, PiPipeconf pipeconf) {
+        return new Builder(deviceId, pipeconf);
+    }
+
+    /**
+     * Builder of P4RuntimeReadResponseImpl.
+     */
+    static final class Builder {
+
+        private final DeviceId deviceId;
+        private final PiPipeconf pipeconf;
+        private final List<PiEntity> entities = Lists.newArrayList();
+        private final ListMultimap<Class<? extends PiEntity>, PiEntity>
+                typeToEntities = ArrayListMultimap.create();
+
+        private boolean success = true;
+        private String explanation;
+        private Throwable throwable;
+
+        private Builder(DeviceId deviceId, PiPipeconf pipeconf) {
+            this.deviceId = deviceId;
+            this.pipeconf = pipeconf;
+        }
+
+        void addEntity(P4RuntimeOuterClass.Entity entityMsg) {
+            try {
+                final PiEntity piEntity = CODECS.entity().decode(entityMsg, null, pipeconf);
+                entities.add(piEntity);
+                typeToEntities.put(piEntity.getClass(), piEntity);
+            } catch (CodecException e) {
+                log.warn("Unable to decode {} message from {}: {} [{}]",
+                         entityMsg.getEntityCase().name(), deviceId,
+                          e.getMessage(), TextFormat.shortDebugString(entityMsg));
+            }
+        }
+
+        ReadResponseImpl fail(Throwable throwable) {
+            checkNotNull(throwable);
+            this.success = false;
+            this.explanation = throwable.getMessage();
+            this.throwable = throwable;
+            return build();
+        }
+
+        ReadResponseImpl build() {
+            if (success && entities.isEmpty()) {
+                return EMPTY;
+            }
+            return new ReadResponseImpl(
+                    success, ImmutableList.copyOf(entities),
+                    ImmutableListMultimap.copyOf(typeToEntities),
+                    explanation, throwable);
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/StreamClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/StreamClientImpl.java
new file mode 100644
index 0000000..7afd97b
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/StreamClientImpl.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.protobuf.TextFormat;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.ClientCallStreamObserver;
+import io.grpc.stub.StreamObserver;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.net.pi.service.PiPipeconfService;
+import org.onosproject.p4runtime.api.P4RuntimeEvent;
+import org.onosproject.p4runtime.api.P4RuntimeStreamClient;
+import org.onosproject.p4runtime.ctl.codec.CodecException;
+import org.onosproject.p4runtime.ctl.controller.ArbitrationUpdateEvent;
+import org.onosproject.p4runtime.ctl.controller.ChannelEvent;
+import org.onosproject.p4runtime.ctl.controller.P4RuntimeControllerImpl;
+import org.onosproject.p4runtime.ctl.controller.PacketInEvent;
+import org.slf4j.Logger;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.StreamMessageRequest;
+import p4.v1.P4RuntimeOuterClass.StreamMessageResponse;
+
+import java.math.BigInteger;
+import java.net.ConnectException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of P4RuntimeStreamClient. Handles P4Runtime StreamChannel RPC
+ * operations, such as arbitration update and packet-in/out.
+ */
+public final class StreamClientImpl implements P4RuntimeStreamClient {
+
+    private static final Logger log = getLogger(StreamClientImpl.class);
+
+    private static final BigInteger ONE_THOUSAND = BigInteger.valueOf(1000);
+
+    private final P4RuntimeClientImpl client;
+    private final DeviceId deviceId;
+    private final long p4DeviceId;
+    private final PiPipeconfService pipeconfService;
+    private final P4RuntimeControllerImpl controller;
+    private final StreamChannelManager streamChannelManager = new StreamChannelManager();
+
+    private P4RuntimeOuterClass.Uint128 lastUsedElectionId = P4RuntimeOuterClass.Uint128
+            .newBuilder().setLow(1).build();
+
+    private final AtomicBoolean isClientMaster = new AtomicBoolean(false);
+
+    StreamClientImpl(
+            PiPipeconfService pipeconfService,
+            P4RuntimeClientImpl client,
+            P4RuntimeControllerImpl controller) {
+        this.client = client;
+        this.deviceId = client.deviceId();
+        this.p4DeviceId = client.p4DeviceId();
+        this.pipeconfService = pipeconfService;
+        this.controller = controller;
+    }
+
+    @Override
+    public void openSession() {
+        if (isSessionOpen()) {
+            log.debug("Dropping request to open session for {}, session is already open",
+                      deviceId);
+            return;
+        }
+        log.debug("Opening session for {}...", deviceId);
+        sendMasterArbitrationUpdate(controller.newMasterElectionId(deviceId));
+
+    }
+
+    @Override
+    public boolean isSessionOpen() {
+        return streamChannelManager.isOpen();
+    }
+
+    @Override
+    public void closeSession() {
+        streamChannelManager.complete();
+    }
+
+    @Override
+    public void runForMastership() {
+        if (!isSessionOpen()) {
+            log.debug("Dropping mastership request for {}, session is closed",
+                      deviceId);
+            return;
+        }
+        // Becoming master is a race. Here we increase our chances of win, i.e.
+        // using the highest election ID, against other ONOS nodes in the
+        // cluster that are calling openSession() (which is used to start the
+        // stream RPC session, not to become master).
+        log.debug("Running for mastership on {}...", deviceId);
+        final BigInteger masterId = controller.newMasterElectionId(deviceId)
+                .add(ONE_THOUSAND);
+        sendMasterArbitrationUpdate(masterId);
+    }
+
+    @Override
+    public boolean isMaster() {
+        return streamChannelManager.isOpen() && isClientMaster.get();
+    }
+
+    @Override
+    public void packetOut(PiPacketOperation packet, PiPipeconf pipeconf) {
+        if (!isSessionOpen()) {
+            log.debug("Dropping packet-out request for {}, session is closed",
+                      deviceId);
+            return;
+        }
+        if (log.isTraceEnabled()) {
+            log.trace("Sending packet-out to {}: {}", deviceId, packet);
+        }
+        try {
+            // Encode the PiPacketOperation into a PacketOut
+            final P4RuntimeOuterClass.PacketOut packetOut =
+                    CODECS.packetOut().encode(packet, null, pipeconf);
+            // Build the request
+            final StreamMessageRequest packetOutRequest = StreamMessageRequest
+                    .newBuilder().setPacket(packetOut).build();
+            // Send.
+            streamChannelManager.sendIfOpen(packetOutRequest);
+        } catch (CodecException e) {
+            log.error("Unable to send packet-out: {}", e.getMessage());
+        }
+    }
+
+    private void sendMasterArbitrationUpdate(BigInteger electionId) {
+        log.debug("Sending arbitration update to {}... electionId={}",
+                  deviceId, electionId);
+        final P4RuntimeOuterClass.Uint128 idMsg = bigIntegerToUint128(electionId);
+        streamChannelManager.send(
+                StreamMessageRequest.newBuilder()
+                        .setArbitration(
+                                P4RuntimeOuterClass.MasterArbitrationUpdate
+                                        .newBuilder()
+                                        .setDeviceId(p4DeviceId)
+                                        .setElectionId(idMsg)
+                                        .build())
+                        .build());
+        lastUsedElectionId = idMsg;
+    }
+
+    private P4RuntimeOuterClass.Uint128 bigIntegerToUint128(BigInteger value) {
+        final byte[] arr = value.toByteArray();
+        final ByteBuffer bb = ByteBuffer.allocate(Long.BYTES * 2)
+                .put(new byte[Long.BYTES * 2 - arr.length])
+                .put(arr);
+        bb.rewind();
+        return P4RuntimeOuterClass.Uint128.newBuilder()
+                .setHigh(bb.getLong())
+                .setLow(bb.getLong())
+                .build();
+    }
+
+    private BigInteger uint128ToBigInteger(P4RuntimeOuterClass.Uint128 value) {
+        return new BigInteger(
+                ByteBuffer.allocate(Long.BYTES * 2)
+                        .putLong(value.getHigh())
+                        .putLong(value.getLow())
+                        .array());
+    }
+
+    private void handlePacketIn(P4RuntimeOuterClass.PacketIn packetInMsg) {
+        if (log.isTraceEnabled()) {
+            log.trace("Received packet-in from {}: {}", deviceId, packetInMsg);
+        }
+        if (!pipeconfService.getPipeconf(deviceId).isPresent()) {
+            log.warn("Unable to handle packet-in from {}, missing pipeconf: {}",
+                     deviceId, TextFormat.shortDebugString(packetInMsg));
+            return;
+        }
+        // Decode packet message and post event.
+        // TODO: consider implementing a cache to speed up
+        //  encoding/deconding of packet-in/out (e.g. LLDP, ARP)
+        final PiPipeconf pipeconf = pipeconfService.getPipeconf(deviceId).get();
+        final PiPacketOperation pktOperation;
+        try {
+            pktOperation = CODECS.packetIn().decode(
+                    packetInMsg, null, pipeconf);
+        } catch (CodecException e) {
+            log.warn("Unable to process packet-int: {}", e.getMessage());
+            return;
+        }
+        controller.postEvent(new P4RuntimeEvent(
+                P4RuntimeEvent.Type.PACKET_IN,
+                new PacketInEvent(deviceId, pktOperation)));
+    }
+
+    private void handleArbitrationUpdate(P4RuntimeOuterClass.MasterArbitrationUpdate msg) {
+        // From the spec...
+        // - Election_id: The stream RPC with the highest election_id is the
+        // master. Switch populates with the highest election ID it
+        // has received from all connected controllers.
+        // - Status: Switch populates this with OK for the client that is the
+        // master, and with an error status for all other connected clients (at
+        // every mastership change).
+        if (!msg.hasElectionId() || !msg.hasStatus()) {
+            return;
+        }
+        final boolean isMaster = msg.getStatus().getCode() == Status.OK.getCode().value();
+        log.debug("Received arbitration update from {}: isMaster={}, electionId={}",
+                  deviceId, isMaster, uint128ToBigInteger(msg.getElectionId()));
+        controller.postEvent(new P4RuntimeEvent(
+                P4RuntimeEvent.Type.ARBITRATION_RESPONSE,
+                new ArbitrationUpdateEvent(deviceId, isMaster)));
+        isClientMaster.set(isMaster);
+    }
+
+    /**
+     * Returns the election ID last used in a MasterArbitrationUpdate message
+     * sent by the client to the server.
+     *
+     * @return election ID uint128 protobuf message
+     */
+    P4RuntimeOuterClass.Uint128 lastUsedElectionId() {
+        return lastUsedElectionId;
+    }
+
+    /**
+     * A manager for the P4Runtime stream channel that opportunistically creates
+     * new stream RCP stubs (e.g. when one fails because of errors) and posts
+     * channel events via the P4Runtime controller.
+     */
+    private final class StreamChannelManager {
+
+        private final AtomicBoolean open = new AtomicBoolean(false);
+        private final StreamObserver<StreamMessageResponse> responseObserver =
+                new InternalStreamResponseObserver(this);
+        private ClientCallStreamObserver<StreamMessageRequest> requestObserver;
+
+        void send(StreamMessageRequest value) {
+            synchronized (this) {
+                initIfRequired();
+                doSend(value);
+            }
+        }
+
+        void sendIfOpen(StreamMessageRequest value) {
+            // We do not lock here, but we ignore NPEs due to stream RPC not
+            // being active (null requestObserver). Good for frequent
+            // packet-outs.
+            try {
+                doSend(value);
+            } catch (NullPointerException e) {
+                if (requestObserver != null) {
+                    // Must be something else.
+                    throw e;
+                }
+            }
+        }
+
+        private void doSend(StreamMessageRequest value) {
+            try {
+                requestObserver.onNext(value);
+            } catch (Throwable ex) {
+                if (ex instanceof StatusRuntimeException) {
+                    log.warn("Unable to send {} to {}: {}",
+                             value.getUpdateCase().toString(), deviceId, ex.getMessage());
+                } else {
+                    log.error("Exception while sending {} to {}: {}",
+                              value.getUpdateCase().toString(), deviceId, ex);
+                }
+                complete();
+            }
+        }
+
+        private void initIfRequired() {
+            if (requestObserver == null) {
+                log.debug("Creating new stream channel for {}...", deviceId);
+                open.set(false);
+                client.execRpcNoTimeout(
+                        s -> requestObserver =
+                                (ClientCallStreamObserver<StreamMessageRequest>)
+                                        s.streamChannel(responseObserver)
+                );
+            }
+        }
+
+        void complete() {
+            synchronized (this) {
+                signalClosed();
+                if (requestObserver != null) {
+                    requestObserver.onCompleted();
+                    requestObserver.cancel("Completed", null);
+                    requestObserver = null;
+                }
+            }
+        }
+
+        void signalOpen() {
+            synchronized (this) {
+                final boolean wasOpen = open.getAndSet(true);
+                if (!wasOpen) {
+                    controller.postEvent(new P4RuntimeEvent(
+                            P4RuntimeEvent.Type.CHANNEL_EVENT,
+                            new ChannelEvent(deviceId, ChannelEvent.Type.OPEN)));
+                }
+            }
+        }
+
+        void signalClosed() {
+            synchronized (this) {
+                final boolean wasOpen = open.getAndSet(false);
+                if (wasOpen) {
+                    controller.postEvent(new P4RuntimeEvent(
+                            P4RuntimeEvent.Type.CHANNEL_EVENT,
+                            new ChannelEvent(deviceId, ChannelEvent.Type.CLOSED)));
+                }
+            }
+        }
+
+        boolean isOpen() {
+            return open.get();
+        }
+    }
+
+    /**
+     * Handles messages received from the device on the stream channel.
+     */
+    private final class InternalStreamResponseObserver
+            implements StreamObserver<StreamMessageResponse> {
+
+        private final StreamChannelManager streamChannelManager;
+
+        private InternalStreamResponseObserver(
+                StreamChannelManager streamChannelManager) {
+            this.streamChannelManager = streamChannelManager;
+        }
+
+        @Override
+        public void onNext(StreamMessageResponse message) {
+            streamChannelManager.signalOpen();
+            try {
+                if (log.isTraceEnabled()) {
+                    log.trace(
+                            "Received {} from {}: {}",
+                            message.getUpdateCase(), deviceId,
+                            TextFormat.shortDebugString(message));
+                }
+                switch (message.getUpdateCase()) {
+                    case PACKET:
+                        handlePacketIn(message.getPacket());
+                        return;
+                    case ARBITRATION:
+                        handleArbitrationUpdate(message.getArbitration());
+                        return;
+                    default:
+                        log.warn("Unrecognized StreamMessageResponse from {}: {}",
+                                 deviceId, message.getUpdateCase());
+                }
+            } catch (Throwable ex) {
+                log.error("Exception while processing stream message from {}",
+                          deviceId, ex);
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            if (throwable instanceof StatusRuntimeException) {
+                final StatusRuntimeException sre = (StatusRuntimeException) throwable;
+                if (sre.getStatus().getCause() instanceof ConnectException) {
+                    log.warn("{} is unreachable ({})",
+                             deviceId, sre.getCause().getMessage());
+                } else {
+                    log.warn("Error on stream channel for {}: {}",
+                             deviceId, throwable.getMessage());
+                }
+            } else {
+                log.error(format("Exception on stream channel for %s",
+                                 deviceId), throwable);
+            }
+            streamChannelManager.complete();
+        }
+
+        @Override
+        public void onCompleted() {
+            log.warn("Stream channel for {} has completed", deviceId);
+            streamChannelManager.complete();
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteRequestImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteRequestImpl.java
new file mode 100644
index 0000000..5b5d087
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteRequestImpl.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.protobuf.TextFormat;
+import io.grpc.stub.StreamObserver;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
+import org.onosproject.p4runtime.ctl.codec.CodecException;
+import org.slf4j.Logger;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.concurrent.CompletableFuture;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.onosproject.p4runtime.ctl.client.P4RuntimeClientImpl.SHORT_TIMEOUT_SECONDS;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Handles the creation of P4Runtime WriteRequest and execution of the Write RPC
+ * on the server.
+ */
+final class WriteRequestImpl implements P4RuntimeWriteClient.WriteRequest {
+
+    private static final Logger log = getLogger(WriteRequestImpl.class);
+
+    private static final P4RuntimeOuterClass.WriteResponse P4RT_DEFAULT_WRITE_RESPONSE_MSG =
+            P4RuntimeOuterClass.WriteResponse.getDefaultInstance();
+
+    private final P4RuntimeClientImpl client;
+    private final PiPipeconf pipeconf;
+    // The P4Runtime WriteRequest protobuf message we need to populate.
+    private final P4RuntimeOuterClass.WriteRequest.Builder requestMsg;
+    // WriteResponse instance builder. We populate entity responses as we add new
+    // entities to this request. The status of each entity response will be
+    // set once we receive a response from the device.
+    private final WriteResponseImpl.Builder responseBuilder;
+
+    WriteRequestImpl(P4RuntimeClientImpl client, PiPipeconf pipeconf) {
+        this.client = checkNotNull(client);
+        this.pipeconf = checkNotNull(pipeconf);
+        this.requestMsg = P4RuntimeOuterClass.WriteRequest.newBuilder()
+                .setDeviceId(client.p4DeviceId());
+        this.responseBuilder = WriteResponseImpl.builder(client.deviceId());
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest withAtomicity(
+            P4RuntimeWriteClient.Atomicity atomicity) {
+        checkNotNull(atomicity);
+        switch (atomicity) {
+            case CONTINUE_ON_ERROR:
+                requestMsg.setAtomicity(
+                        P4RuntimeOuterClass.WriteRequest.Atomicity.CONTINUE_ON_ERROR);
+                break;
+            case ROLLBACK_ON_ERROR:
+            case DATAPLANE_ATOMIC:
+                // Supporting this while allowing codec exceptions to be
+                // reported as write responses can be tricky. Assuming write on
+                // device succeed but we have a codec exception and
+                // atomicity is rollback on error.
+            default:
+                throw new UnsupportedOperationException(format(
+                        "Atomicity mode %s not supported", atomicity));
+        }
+        return this;
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest insert(PiEntity entity) {
+        return entity(entity, P4RuntimeWriteClient.UpdateType.INSERT);
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest insert(
+            Iterable<? extends PiEntity> entities) {
+        return entities(entities, P4RuntimeWriteClient.UpdateType.INSERT);
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest modify(PiEntity entity) {
+        return entity(entity, P4RuntimeWriteClient.UpdateType.MODIFY);
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest modify(
+            Iterable<? extends PiEntity> entities) {
+        return entities(entities, P4RuntimeWriteClient.UpdateType.MODIFY);
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest delete(
+            Iterable<? extends PiHandle> handles) {
+        checkNotNull(handles);
+        handles.forEach(this::delete);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest entities(
+            Iterable<? extends PiEntity> entities,
+            P4RuntimeWriteClient.UpdateType updateType) {
+        checkNotNull(entities);
+        entities.forEach(e -> this.entity(e, updateType));
+        return this;
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest entity(
+            PiEntity entity, P4RuntimeWriteClient.UpdateType updateType) {
+        checkNotNull(entity);
+        checkNotNull(updateType);
+        appendToRequestMsg(updateType, entity, entity.handle(client.deviceId()));
+        return this;
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteRequest delete(PiHandle handle) {
+        checkNotNull(handle);
+        appendToRequestMsg(P4RuntimeWriteClient.UpdateType.DELETE, null, handle);
+        return this;
+    }
+
+    @Override
+    public P4RuntimeWriteClient.WriteResponse submitSync() {
+        return Futures.getUnchecked(submit());
+    }
+
+    @Override
+    public CompletableFuture<P4RuntimeWriteClient.WriteResponse> submit() {
+        final P4RuntimeOuterClass.WriteRequest writeRequest = requestMsg
+                .setElectionId(client.lastUsedElectionId())
+                .build();
+        log.debug("Sending write request to {} with {} updates...",
+                  client.deviceId(), writeRequest.getUpdatesCount());
+        if (writeRequest.getUpdatesCount() == 0) {
+            // No need to ask the server.
+            return completedFuture(WriteResponseImpl.EMPTY);
+        }
+        final CompletableFuture<P4RuntimeWriteClient.WriteResponse> future =
+                new CompletableFuture<>();
+        final StreamObserver<P4RuntimeOuterClass.WriteResponse> observer =
+                new StreamObserver<P4RuntimeOuterClass.WriteResponse>() {
+                    @Override
+                    public void onNext(P4RuntimeOuterClass.WriteResponse value) {
+                        if (!P4RT_DEFAULT_WRITE_RESPONSE_MSG.equals(value)) {
+                            log.warn("Received invalid WriteResponse message from {}: {}",
+                                     client.deviceId(), TextFormat.shortDebugString(value));
+                            // Leave all entity responses in pending state.
+                            future.complete(responseBuilder.buildAsIs());
+                        } else {
+                            log.debug("Received write response from {}...",
+                                      client.deviceId());
+                            // All good, all entities written successfully.
+                            future.complete(responseBuilder.setSuccessAllAndBuild());
+                        }
+                    }
+                    @Override
+                    public void onError(Throwable t) {
+                        client.handleRpcError(t, "WRITE");
+                        future.complete(responseBuilder.setErrorsAndBuild(t));
+                    }
+                    @Override
+                    public void onCompleted() {
+                        // Nothing to do, unary call.
+                    }
+                };
+        client.execRpc(s -> s.write(writeRequest, observer), SHORT_TIMEOUT_SECONDS);
+        return future;
+    }
+
+    private void appendToRequestMsg(P4RuntimeWriteClient.UpdateType updateType,
+                                    PiEntity piEntity, PiHandle handle) {
+        final P4RuntimeOuterClass.Update.Type p4UpdateType;
+        final P4RuntimeOuterClass.Entity entityMsg;
+        try {
+            if (updateType.equals(P4RuntimeWriteClient.UpdateType.DELETE)) {
+                p4UpdateType = P4RuntimeOuterClass.Update.Type.DELETE;
+                entityMsg = CODECS.handle().encode(handle, null, pipeconf);
+            } else {
+                p4UpdateType = updateType == P4RuntimeWriteClient.UpdateType.INSERT
+                        ? P4RuntimeOuterClass.Update.Type.INSERT
+                        : P4RuntimeOuterClass.Update.Type.MODIFY;
+                entityMsg = CODECS.entity().encode(piEntity, null, pipeconf);
+            }
+            final P4RuntimeOuterClass.Update updateMsg = P4RuntimeOuterClass.Update
+                    .newBuilder()
+                    .setEntity(entityMsg)
+                    .setType(p4UpdateType)
+                    .build();
+            requestMsg.addUpdates(updateMsg);
+            responseBuilder.addPendingResponse(handle, piEntity, updateType);
+        } catch (CodecException e) {
+            responseBuilder.addFailedResponse(
+                    handle, piEntity, updateType, e.getMessage(),
+                    P4RuntimeWriteClient.WriteResponseStatus.CODEC_ERROR);
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteResponseImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteResponseImpl.java
new file mode 100644
index 0000000..404ab80
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/WriteResponseImpl.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.client;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.protobuf.Any;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.TextFormat;
+import io.grpc.Metadata;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.protobuf.lite.ProtoLiteUtils;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteEntityResponse;
+import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteResponseStatus;
+import org.slf4j.Logger;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Handles the creation of WriteResponse and parsing of P4Runtime errors
+ * received from server, as well as logging of RPC errors.
+ */
+final class WriteResponseImpl implements P4RuntimeWriteClient.WriteResponse {
+
+    private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
+            Metadata.Key.of(
+                    "grpc-status-details-bin",
+                    ProtoLiteUtils.metadataMarshaller(
+                            com.google.rpc.Status.getDefaultInstance()));
+
+    static final WriteResponseImpl EMPTY = new WriteResponseImpl(
+            ImmutableList.of(), ImmutableListMultimap.of());
+
+    private static final Logger log = getLogger(WriteResponseImpl.class);
+
+    private final ImmutableList<WriteEntityResponse> entityResponses;
+    private final ImmutableListMultimap<WriteResponseStatus, WriteEntityResponse> statusMultimap;
+
+    private WriteResponseImpl(
+            ImmutableList<WriteEntityResponse> allResponses,
+            ImmutableListMultimap<WriteResponseStatus, WriteEntityResponse> statusMultimap) {
+        this.entityResponses = allResponses;
+        this.statusMultimap = statusMultimap;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return success().size() == all().size();
+    }
+
+    @Override
+    public Collection<WriteEntityResponse> all() {
+        return entityResponses;
+    }
+
+    @Override
+    public Collection<WriteEntityResponse> success() {
+        return statusMultimap.get(WriteResponseStatus.OK);
+    }
+
+    @Override
+    public Collection<WriteEntityResponse> failed() {
+        return isSuccess()
+                ? Collections.emptyList()
+                : entityResponses.stream().filter(r -> !r.isSuccess()).collect(toList());
+    }
+
+    @Override
+    public Collection<WriteEntityResponse> status(
+            WriteResponseStatus status) {
+        checkNotNull(status);
+        return statusMultimap.get(status);
+    }
+
+    /**
+     * Returns a new response builder for the given device.
+     *
+     * @param deviceId device ID
+     * @return response builder
+     */
+    static Builder builder(DeviceId deviceId) {
+        return new Builder(deviceId);
+    }
+
+    /**
+     * Builder of P4RuntimeWriteResponseImpl.
+     */
+    static final class Builder {
+
+        private final DeviceId deviceId;
+        private final Map<Integer, WriteEntityResponseImpl> pendingResponses =
+                Maps.newHashMap();
+        private final List<WriteEntityResponse> allResponses =
+                Lists.newArrayList();
+        private final ListMultimap<WriteResponseStatus, WriteEntityResponse> statusMap =
+                ArrayListMultimap.create();
+
+        private Builder(DeviceId deviceId) {
+            this.deviceId = deviceId;
+        }
+
+        void addPendingResponse(PiHandle handle, PiEntity entity, UpdateType updateType) {
+            synchronized (this) {
+                final WriteEntityResponseImpl resp = new WriteEntityResponseImpl(
+                        handle, entity, updateType);
+                allResponses.add(resp);
+                pendingResponses.put(pendingResponses.size(), resp);
+            }
+        }
+
+        void addFailedResponse(PiHandle handle, PiEntity entity, UpdateType updateType,
+                               String explanation, WriteResponseStatus status) {
+            synchronized (this) {
+                final WriteEntityResponseImpl resp = new WriteEntityResponseImpl(
+                        handle, entity, updateType)
+                        .withFailure(explanation, status);
+                allResponses.add(resp);
+            }
+        }
+
+        WriteResponseImpl buildAsIs() {
+            synchronized (this) {
+                if (!pendingResponses.isEmpty()) {
+                    log.warn("Detected partial response from {}, " +
+                                     "{} of {} total entities are in status PENDING",
+                             deviceId, pendingResponses.size(), allResponses.size());
+                }
+                return new WriteResponseImpl(
+                        ImmutableList.copyOf(allResponses),
+                        ImmutableListMultimap.copyOf(statusMap));
+            }
+        }
+
+        WriteResponseImpl setSuccessAllAndBuild() {
+            synchronized (this) {
+                pendingResponses.values().forEach(this::doSetSuccess);
+                pendingResponses.clear();
+                return buildAsIs();
+            }
+        }
+
+        WriteResponseImpl setErrorsAndBuild(Throwable throwable) {
+            synchronized (this) {
+                return doSetErrorsAndBuild(throwable);
+            }
+        }
+
+        private void setSuccess(int index) {
+            synchronized (this) {
+                final WriteEntityResponseImpl resp = pendingResponses.remove(index);
+                if (resp != null) {
+                    doSetSuccess(resp);
+                } else {
+                    log.error("Missing pending response at index {}", index);
+                }
+            }
+        }
+
+        private void doSetSuccess(WriteEntityResponseImpl resp) {
+            resp.setSuccess();
+            statusMap.put(WriteResponseStatus.OK, resp);
+        }
+
+        private void setFailure(int index,
+                                String explanation,
+                                WriteResponseStatus status) {
+            synchronized (this) {
+                final WriteEntityResponseImpl resp = pendingResponses.remove(index);
+                if (resp != null) {
+                    resp.withFailure(explanation, status);
+                    statusMap.put(status, resp);
+                    log.warn("Unable to {} {} on {}: {} {} [{}]",
+                             resp.updateType(),
+                             resp.entityType().humanReadableName(),
+                             deviceId,
+                             status, explanation,
+                             resp.entity() != null ? resp.entity() : resp.handle());
+                } else {
+                    log.error("Missing pending response at index {}", index);
+                }
+            }
+        }
+
+        private WriteResponseImpl doSetErrorsAndBuild(Throwable throwable) {
+            if (!(throwable instanceof StatusRuntimeException)) {
+                // Leave all entity responses in pending state.
+                return buildAsIs();
+            }
+            final StatusRuntimeException sre = (StatusRuntimeException) throwable;
+            if (!sre.getStatus().equals(Status.UNKNOWN)) {
+                // Error trailers expected only if status is UNKNOWN.
+                return buildAsIs();
+            }
+            // Extract error details.
+            if (!sre.getTrailers().containsKey(STATUS_DETAILS_KEY)) {
+                log.warn("Cannot parse write error details from {}, " +
+                                 "missing status trailers in StatusRuntimeException",
+                         deviceId);
+                return buildAsIs();
+            }
+            com.google.rpc.Status status = sre.getTrailers().get(STATUS_DETAILS_KEY);
+            if (status == null) {
+                log.warn("Cannot parse write error details from {}, " +
+                                 "found NULL status trailers in StatusRuntimeException",
+                         deviceId);
+                return buildAsIs();
+            }
+            final boolean reconcilable = status.getDetailsList().size() == pendingResponses.size();
+            // We expect one error for each entity...
+            if (!reconcilable) {
+                log.warn("Unable to reconcile write error details from {}, " +
+                                 "sent {} updates, but server returned {} errors",
+                         deviceId, pendingResponses.size(), status.getDetailsList().size());
+            }
+            // ...in the same order as in the request.
+            int index = 0;
+            for (Any any : status.getDetailsList()) {
+                // Set response entities only if reconcilable, otherwise log.
+                unpackP4Error(index, any, reconcilable);
+                index += 1;
+            }
+            return buildAsIs();
+        }
+
+        private void unpackP4Error(int index, Any any, boolean reconcilable) {
+            final P4RuntimeOuterClass.Error p4Error;
+            try {
+                p4Error = any.unpack(P4RuntimeOuterClass.Error.class);
+            } catch (InvalidProtocolBufferException e) {
+                final String unpackErr = format(
+                        "P4Runtime Error message format not recognized [%s]",
+                        TextFormat.shortDebugString(any));
+                if (reconcilable) {
+                    setFailure(index, unpackErr, WriteResponseStatus.OTHER_ERROR);
+                } else {
+                    log.warn(unpackErr);
+                }
+                return;
+            }
+            // Map gRPC status codes to our WriteResponseStatus codes.
+            final Status.Code p4Code = Status.fromCodeValue(
+                    p4Error.getCanonicalCode()).getCode();
+            final WriteResponseStatus ourCode;
+            switch (p4Code) {
+                case OK:
+                    if (reconcilable) {
+                        setSuccess(index);
+                    }
+                    return;
+                case NOT_FOUND:
+                    ourCode = WriteResponseStatus.NOT_FOUND;
+                    break;
+                case ALREADY_EXISTS:
+                    ourCode = WriteResponseStatus.ALREADY_EXIST;
+                    break;
+                default:
+                    ourCode = WriteResponseStatus.OTHER_ERROR;
+                    break;
+            }
+            // Put the p4Code in the explanation only if ourCode is OTHER_ERROR.
+            final String explanationCode = ourCode == WriteResponseStatus.OTHER_ERROR
+                    ? p4Code.name() + " " : "";
+            final String details = p4Error.hasDetails()
+                    ? ", " + p4Error.getDetails().toString() : "";
+            final String explanation = format(
+                    "%s%s%s (%s:%d)", explanationCode, p4Error.getMessage(),
+                    details, p4Error.getSpace(), p4Error.getCode());
+            if (reconcilable) {
+                setFailure(index, explanation, ourCode);
+            } else {
+                log.warn("P4Runtime write error: {}", explanation);
+            }
+        }
+    }
+
+    /**
+     * Internal implementation of WriteEntityResponse.
+     */
+    private static final class WriteEntityResponseImpl implements WriteEntityResponse {
+
+        private final PiHandle handle;
+        private final PiEntity entity;
+        private final UpdateType updateType;
+
+        private WriteResponseStatus status = WriteResponseStatus.PENDING;
+        private String explanation;
+        private Throwable throwable;
+
+        private WriteEntityResponseImpl(PiHandle handle, PiEntity entity, UpdateType updateType) {
+            this.handle = handle;
+            this.entity = entity;
+            this.updateType = updateType;
+        }
+
+        private WriteEntityResponseImpl withFailure(
+                String explanation, WriteResponseStatus status) {
+            this.status = status;
+            this.explanation = explanation;
+            this.throwable = null;
+            return this;
+        }
+
+        private void setSuccess() {
+            this.status = WriteResponseStatus.OK;
+        }
+
+        @Override
+        public PiHandle handle() {
+            return handle;
+        }
+
+        @Override
+        public PiEntity entity() {
+            return entity;
+        }
+
+        @Override
+        public UpdateType updateType() {
+            return updateType;
+        }
+
+        @Override
+        public PiEntityType entityType() {
+            return handle.entityType();
+        }
+
+        @Override
+        public boolean isSuccess() {
+            return status().equals(WriteResponseStatus.OK);
+        }
+
+        @Override
+        public WriteResponseStatus status() {
+            return status;
+        }
+
+        @Override
+        public String explanation() {
+            return explanation;
+        }
+
+        @Override
+        public Throwable throwable() {
+            return throwable;
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("handle", handle)
+                    .add("entity", entity)
+                    .add("updateType", updateType)
+                    .add("status", status)
+                    .add("explanation", explanation)
+                    .add("throwable", throwable)
+                    .toString();
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/package-info.java
similarity index 62%
copy from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
copy to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/package-info.java
index 89d5510..a5614a3 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/client/package-info.java
@@ -14,18 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
-
 /**
- * Signals an error during encoding/decoding of a PI entity/protobuf message.
+ * P4Runtime client implementation classes.
  */
-public final class CodecException extends Exception {
-
-    /**
-     * Ceeates anew exception with the given explanation message.
-     * @param explanation explanation
-     */
-    public CodecException(String explanation) {
-        super(explanation);
-    }
-}
+package org.onosproject.p4runtime.ctl.client;
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java
new file mode 100644
index 0000000..2bb75a3
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import org.onosproject.p4runtime.ctl.utils.PipeconfHelper;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Abstract implementation of a general codec that translates pipeconf-related
+ * objects into protobuf messages and vice versa.
+ *
+ * @param <P> object
+ * @param <M> protobuf message class
+ * @param <X> metadata class
+ */
+abstract class AbstractCodec<P, M extends Message, X> {
+
+    protected final Logger log = getLogger(this.getClass());
+
+    protected abstract M encode(P object, X metadata, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract P decode(M message, X metadata, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a protobuf message that is equivalent to the given object for the
+     * given metadata and pipeconf.
+     *
+     * @param object   object
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return protobuf message
+     * @throws CodecException if the given object cannot be encoded (see
+     *                        exception message)
+     */
+    public M encode(P object, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(object);
+        try {
+            return encode(object, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a object that is equivalent to the protobuf message for the given
+     * metadata and pipeconf.
+     *
+     * @param message  protobuf message
+     * @param metadata metadata
+     * @param pipeconf pipeconf pipeconf
+     * @return object
+     * @throws CodecException if the given protobuf message cannot be decoded
+     *                        (see exception message)
+     */
+    public P decode(M message, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(message);
+        try {
+            return decode(message, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Same as {@link #encode(Object, Object, PiPipeconf)} but returns null in
+     * case of exceptions, while the error message is logged.
+     *
+     * @param object   object
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return protobuf message
+     */
+    private M encodeOrNull(P object, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(object);
+        try {
+            return encode(object, metadata, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to encode {}: {} [{}]",
+                      object.getClass().getSimpleName(),
+                      e.getMessage(), object.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Same as {@link #decode(Message, Object, PiPipeconf)} but returns null in
+     * case of exceptions, while the error message is logged.
+     *
+     * @param message  protobuf message
+     * @param metadata metadata
+     * @param pipeconf pipeconf pipeconf
+     * @return object
+     */
+    private P decodeOrNull(M message, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(message);
+        try {
+            return decode(message, metadata, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to decode {}: {} [{}]",
+                      message.getClass().getSimpleName(),
+                      e.getMessage(), TextFormat.shortDebugString(message));
+            return null;
+        }
+    }
+
+    /**
+     * Encodes the given list of objects, skipping those that cannot be encoded,
+     * in which case an error message is logged. For this reason, the returned
+     * list might have different size than the returned one.
+     *
+     * @param objects  list of objects
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of protobuf messages
+     */
+    private List<M> encodeAllSkipException(
+            Collection<P> objects, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(objects);
+        if (objects.isEmpty()) {
+            return ImmutableList.of();
+        }
+        return objects.stream()
+                .map(p -> encodeOrNull(p, metadata, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Decodes the given list of protobuf messages, skipping those that cannot
+     * be decoded, on which case an error message is logged. For this reason,
+     * the returned list might have different size than the returned one.
+     *
+     * @param messages list of protobuf messages
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of objects
+     */
+    private List<P> decodeAllSkipException(
+            Collection<M> messages, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(messages);
+        if (messages.isEmpty()) {
+            return ImmutableList.of();
+        }
+        return messages.stream()
+                .map(m -> decodeOrNull(m, metadata, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Encodes the given collection of objects. Throws an exception if one or
+     * more of the given objects cannot be encoded. The returned list is
+     * guaranteed to have same size and order as the given one.
+     *
+     * @param objects  list of objects
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of protobuf messages
+     * @throws CodecException if one or more of the given objects cannot be
+     *                        encoded
+     */
+    List<M> encodeAll(Collection<P> objects, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(objects);
+        if (objects.isEmpty()) {
+            return ImmutableList.of();
+        }
+        final List<M> messages = encodeAllSkipException(objects, metadata, pipeconf);
+        if (objects.size() != messages.size()) {
+            throw new CodecException(format(
+                    "Unable to encode %d entities of %d given " +
+                            "(see previous logs for details)",
+                    objects.size() - messages.size(), objects.size()));
+        }
+        return messages;
+    }
+
+    /**
+     * Decodes the given collection of protobuf messages. Throws an exception if
+     * one or more of the given protobuf messages cannot be decoded. The
+     * returned list is guaranteed to have same size and order as the given
+     * one.
+     *
+     * @param messages list of protobuf messages
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of objects
+     * @throws CodecException if one or more of the given protobuf messages
+     *                        cannot be decoded
+     */
+    List<P> decodeAll(Collection<M> messages, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(messages);
+        if (messages.isEmpty()) {
+            return ImmutableList.of();
+        }
+        final List<P> objects = decodeAllSkipException(messages, metadata, pipeconf);
+        if (messages.size() != objects.size()) {
+            throw new CodecException(format(
+                    "Unable to decode %d messages of %d given " +
+                            "(see previous logs for details)",
+                    messages.size() - objects.size(), messages.size()));
+        }
+        return objects;
+    }
+
+    /**
+     * Returns a P4Info browser for the given pipeconf or throws a
+     * CodecException if not possible.
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info browser
+     * @throws CodecException if a P4Info browser cannot be obtained
+     */
+    P4InfoBrowser browserOrFail(PiPipeconf pipeconf) throws CodecException {
+        checkNotNull(pipeconf);
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new CodecException(format(
+                    "Unable to get P4InfoBrowser for pipeconf %s", pipeconf.id()));
+        }
+        return browser;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java
new file mode 100644
index 0000000..0b39367
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.protobuf.Message;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstract implementation of a specialized codec that translates PI runtime
+ * entities and their handles into P4Runtime protobuf messages and vice versa.
+ * Supports also encoding to "key" P4Runtime Entity messages used in read and
+ * delete operations.
+ *
+ * @param <P> PI runtime class
+ * @param <H> PI handle class
+ * @param <M> P4Runtime protobuf message class
+ * @param <X> metadata class
+ */
+public abstract class AbstractEntityCodec
+        <P extends PiEntity, H extends PiHandle, M extends Message, X>
+        extends AbstractCodec<P, M, X> {
+
+    protected abstract M encodeKey(H handle, X metadata, PiPipeconf pipeconf,
+                                   P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract M encodeKey(P piEntity, X metadata, PiPipeconf pipeconf,
+                                   P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a P4Runtime protobuf message representing the P4Runtime.Entity
+     * "key" for the given PI handle, metadata and pipeconf.
+     *
+     * @param handle   PI handle instance
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encodeKey(H handle, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(handle);
+        try {
+            return encodeKey(handle, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a P4Runtime protobuf message representing the P4Runtime.Entity
+     * "key" for the given PI entity, metadata and pipeconf.
+     *
+     * @param piEntity PI entity instance
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encodeKey(P piEntity, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(piEntity);
+        try {
+            return encodeKey(piEntity, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java
new file mode 100644
index 0000000..3ae46c9
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiActionId;
+import org.onosproject.net.pi.model.PiActionParamId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertSize;
+
+/**
+ * Codec for P4Runtime Action.
+ */
+public final class ActionCodec
+        extends AbstractCodec<PiAction, P4RuntimeOuterClass.Action, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Action encode(
+            PiAction piAction, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final int actionId = browser.actions()
+                .getByName(piAction.id().toString()).getPreamble().getId();
+        final P4RuntimeOuterClass.Action.Builder actionMsgBuilder =
+                P4RuntimeOuterClass.Action.newBuilder().setActionId(actionId);
+        for (PiActionParam p : piAction.parameters()) {
+            final P4InfoOuterClass.Action.Param paramInfo = browser.actionParams(actionId)
+                    .getByName(p.id().toString());
+            final ByteString paramValue = ByteString.copyFrom(p.value().asReadOnlyBuffer());
+            assertSize(format("param '%s' of action '%s'", p.id(), piAction.id()),
+                       paramValue, paramInfo.getBitwidth());
+            actionMsgBuilder.addParams(P4RuntimeOuterClass.Action.Param.newBuilder()
+                                               .setParamId(paramInfo.getId())
+                                               .setValue(paramValue)
+                                               .build());
+        }
+        return actionMsgBuilder.build();
+    }
+
+    @Override
+    protected PiAction decode(
+            P4RuntimeOuterClass.Action message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final P4InfoBrowser.EntityBrowser<P4InfoOuterClass.Action.Param> paramInfo =
+                browser.actionParams(message.getActionId());
+        final String actionName = browser.actions()
+                .getById(message.getActionId())
+                .getPreamble().getName();
+        final PiAction.Builder builder = PiAction.builder()
+                .withId(PiActionId.of(actionName));
+        for (P4RuntimeOuterClass.Action.Param p : message.getParamsList()) {
+            final String paramName = paramInfo.getById(p.getParamId()).getName();
+            final ImmutableByteSequence value = ImmutableByteSequence.copyFrom(
+                    p.getValue().toByteArray());
+            builder.withParameter(new PiActionParam(PiActionParamId.of(paramName), value));
+        }
+        return builder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java
new file mode 100644
index 0000000..fc74df7
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
+
+/**
+ * Codec for P4Runtime ActionProfileGroup.
+ */
+public final class ActionProfileGroupCodec
+        extends AbstractEntityCodec<PiActionProfileGroup, PiActionProfileGroupHandle, ActionProfileGroup, Object> {
+
+    @Override
+    public ActionProfileGroup encode(
+            PiActionProfileGroup piGroup, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final ActionProfileGroup.Builder msgBuilder = keyMsgBuilder(
+                piGroup.actionProfile(), piGroup.id(), browser)
+                .setMaxSize(piGroup.maxSize());
+        piGroup.members().forEach(m -> {
+            // TODO: currently we don't set "watch" field
+            ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
+                    .setMemberId(m.id().id())
+                    .setWeight(m.weight())
+                    .build();
+            msgBuilder.addMembers(member);
+        });
+        return msgBuilder.build();
+    }
+
+    @Override
+    protected ActionProfileGroup encodeKey(
+            PiActionProfileGroupHandle handle, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.actionProfile(), handle.groupId(), browser)
+                .build();
+    }
+
+    @Override
+    protected ActionProfileGroup encodeKey(
+            PiActionProfileGroup piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.actionProfile(), piEntity.id(), browser)
+                .build();
+    }
+
+    private ActionProfileGroup.Builder keyMsgBuilder(
+            PiActionProfileId actProfId, PiActionProfileGroupId groupId,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return ActionProfileGroup.newBuilder()
+                .setGroupId(groupId.id())
+                .setActionProfileId(browser.actionProfiles()
+                                            .getByName(actProfId.id())
+                                            .getPreamble().getId());
+    }
+
+    @Override
+    public PiActionProfileGroup decode(
+            ActionProfileGroup msg, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final PiActionProfileGroup.Builder piGroupBuilder = PiActionProfileGroup.builder()
+                .withActionProfileId(PiActionProfileId.of(
+                        browser.actionProfiles()
+                                .getById(msg.getActionProfileId())
+                                .getPreamble().getName()))
+                .withId(PiActionProfileGroupId.of(msg.getGroupId()))
+                .withMaxSize(msg.getMaxSize());
+        msg.getMembersList().forEach(m -> {
+            int weight = m.getWeight();
+            if (weight < 1) {
+                // FIXME: PI has a bug which will always return weight 0
+                // ONOS won't accept group buckets with weight 0
+                log.debug("Decoding ActionProfileGroup with 'weight' " +
+                                 "field {}, will set to 1", weight);
+                weight = 1;
+            }
+            piGroupBuilder.addMember(PiActionProfileMemberId.of(
+                    m.getMemberId()), weight);
+        });
+        return piGroupBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java
new file mode 100644
index 0000000..183d827
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for ActionProfileMember.
+ */
+public final class ActionProfileMemberCodec
+        extends AbstractEntityCodec<PiActionProfileMember, PiActionProfileMemberHandle, ActionProfileMember, Object> {
+
+    @Override
+    public ActionProfileMember encode(
+            PiActionProfileMember piEntity, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(
+                piEntity.actionProfile(), piEntity.id(), browser)
+                .setAction(CODECS.action().encode(
+                        piEntity.action(), null, pipeconf))
+                .build();
+    }
+
+    @Override
+    protected ActionProfileMember encodeKey(
+            PiActionProfileMemberHandle handle, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.actionProfileId(), handle.memberId(), browser)
+                .build();
+    }
+
+    @Override
+    protected ActionProfileMember encodeKey(
+            PiActionProfileMember piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(
+                piEntity.actionProfile(), piEntity.id(), browser)
+                .build();
+    }
+
+    private P4RuntimeOuterClass.ActionProfileMember.Builder keyMsgBuilder(
+            PiActionProfileId actProfId, PiActionProfileMemberId memberId,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return P4RuntimeOuterClass.ActionProfileMember.newBuilder()
+                .setActionProfileId(browser.actionProfiles()
+                                            .getByName(actProfId.id())
+                                            .getPreamble().getId())
+                .setMemberId(memberId.id());
+    }
+
+    @Override
+    public PiActionProfileMember decode(
+            ActionProfileMember message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final PiActionProfileId actionProfileId = PiActionProfileId.of(
+                browser.actionProfiles()
+                        .getById(message.getActionProfileId())
+                        .getPreamble()
+                        .getName());
+        return PiActionProfileMember.builder()
+                .forActionProfile(actionProfileId)
+                .withId(PiActionProfileMemberId.of(message.getMemberId()))
+                .withAction(CODECS.action().decode(
+                        message.getAction(), null, pipeconf))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java
similarity index 88%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java
index 89d5510..6ef77f2 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.codec;
 
 /**
  * Signals an error during encoding/decoding of a PI entity/protobuf message.
@@ -22,7 +22,8 @@
 public final class CodecException extends Exception {
 
     /**
-     * Ceeates anew exception with the given explanation message.
+     * Creates a new exception with the given explanation message.
+     *
      * @param explanation explanation
      */
     public CodecException(String explanation) {
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java
new file mode 100644
index 0000000..771f5da
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+/**
+ * Utility class that provides access to P4Runtime codec instances.
+ */
+public final class Codecs {
+
+    public static final Codecs CODECS = new Codecs();
+
+    private final ActionCodec action;
+    private final ActionProfileGroupCodec actionProfileGroup;
+    private final ActionProfileMemberCodec actionProfileMember;
+    private final CounterEntryCodec counterEntry;
+    private final DirectCounterEntryCodec directCounterEntry;
+    private final DirectMeterEntryCodec directMeterEntry;
+    private final EntityCodec entity;
+    private final FieldMatchCodec fieldMatch;
+    private final HandleCodec handle;
+    private final MeterEntryCodec meterEntry;
+    private final MulticastGroupEntryCodec multicastGroupEntry;
+    private final PacketInCodec packetIn;
+    private final PacketMetadataCodec packetMetadata;
+    private final PacketOutCodec packetOut;
+    private final TableEntryCodec tableEntry;
+
+    private Codecs() {
+        this.action = new ActionCodec();
+        this.actionProfileGroup = new ActionProfileGroupCodec();
+        this.actionProfileMember = new ActionProfileMemberCodec();
+        this.counterEntry = new CounterEntryCodec();
+        this.directCounterEntry = new DirectCounterEntryCodec();
+        this.directMeterEntry = new DirectMeterEntryCodec();
+        this.entity = new EntityCodec();
+        this.fieldMatch = new FieldMatchCodec();
+        this.handle = new HandleCodec();
+        this.meterEntry = new MeterEntryCodec();
+        this.multicastGroupEntry = new MulticastGroupEntryCodec();
+        this.packetIn = new PacketInCodec();
+        this.packetMetadata = new PacketMetadataCodec();
+        this.packetOut = new PacketOutCodec();
+        this.tableEntry = new TableEntryCodec();
+    }
+
+    public EntityCodec entity() {
+        return entity;
+    }
+
+    public HandleCodec handle() {
+        return handle;
+    }
+
+    public PacketOutCodec packetOut() {
+        return packetOut;
+    }
+
+    public PacketInCodec packetIn() {
+        return packetIn;
+    }
+
+    TableEntryCodec tableEntry() {
+        return tableEntry;
+    }
+
+    FieldMatchCodec fieldMatch() {
+        return fieldMatch;
+    }
+
+    ActionCodec action() {
+        return action;
+    }
+
+    ActionProfileMemberCodec actionProfileMember() {
+        return actionProfileMember;
+    }
+
+    ActionProfileGroupCodec actionProfileGroup() {
+        return actionProfileGroup;
+    }
+
+    PacketMetadataCodec packetMetadata() {
+        return packetMetadata;
+    }
+
+    MulticastGroupEntryCodec multicastGroupEntry() {
+        return multicastGroupEntry;
+    }
+
+    DirectMeterEntryCodec directMeterEntry() {
+        return directMeterEntry;
+    }
+
+    MeterEntryCodec meterEntry() {
+        return meterEntry;
+    }
+
+    CounterEntryCodec counterEntry() {
+        return counterEntry;
+    }
+
+    DirectCounterEntryCodec directCounterEntry() {
+        return directCounterEntry;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java
new file mode 100644
index 0000000..deac993
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiCounterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+/**
+ * Codec for P4Runtime CounterEntry.
+ */
+public final class CounterEntryCodec
+        extends AbstractEntityCodec<PiCounterCell, PiCounterCellHandle,
+        P4RuntimeOuterClass.CounterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encode(
+            PiCounterCell piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser)
+                .setData(P4RuntimeOuterClass.CounterData.newBuilder()
+                                 .setByteCount(piEntity.data().bytes())
+                                 .setPacketCount(piEntity.data().packets())
+                                 .build())
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encodeKey(
+            PiCounterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.cellId(), browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encodeKey(
+            PiCounterCell piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser).build();
+    }
+
+    private P4RuntimeOuterClass.CounterEntry.Builder keyMsgBuilder(
+            PiCounterCellId cellId, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int counterId = browser.counters().getByName(
+                cellId.counterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.CounterEntry.newBuilder()
+                .setCounterId(counterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(cellId.index()).build());
+    }
+
+    @Override
+    protected PiCounterCell decode(
+            P4RuntimeOuterClass.CounterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String counterName = browser.counters()
+                .getById(message.getCounterId())
+                .getPreamble()
+                .getName();
+        return new PiCounterCell(
+                PiCounterCellId.ofIndirect(
+                        PiCounterId.of(counterName), message.getIndex().getIndex()),
+                message.getData().getPacketCount(),
+                message.getData().getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java
new file mode 100644
index 0000000..46d0f3f
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime DirectCounterEntryCodec.
+ */
+public final class DirectCounterEntryCodec
+        extends AbstractEntityCodec<PiCounterCell, PiCounterCellHandle,
+        P4RuntimeOuterClass.DirectCounterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encode(
+            PiCounterCell piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf)
+                .setData(P4RuntimeOuterClass.CounterData.newBuilder()
+                                 .setByteCount(piEntity.data().bytes())
+                                 .setPacketCount(piEntity.data().packets())
+                                 .build())
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encodeKey(
+            PiCounterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(handle.cellId(), pipeconf).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encodeKey(
+            PiCounterCell piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf).build();
+    }
+
+    private P4RuntimeOuterClass.DirectCounterEntry.Builder keyMsgBuilder(
+            PiCounterCellId cellId, PiPipeconf pipeconf)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectCounterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encodeKey(
+                        cellId.tableEntry(), null, pipeconf));
+    }
+
+    @Override
+    protected PiCounterCell decode(
+            P4RuntimeOuterClass.DirectCounterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return new PiCounterCell(
+                PiCounterCellId.ofDirect(
+                        CODECS.tableEntry().decode(
+                                message.getTableEntry(), null, pipeconf)),
+                message.getData().getPacketCount(),
+                message.getData().getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java
new file mode 100644
index 0000000..3bedfbf
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMeterBand;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime DirectMeterEntryCodec.
+ */
+public final class DirectMeterEntryCodec
+        extends AbstractEntityCodec<PiMeterCellConfig, PiMeterCellHandle,
+        P4RuntimeOuterClass.DirectMeterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encode(
+            PiMeterCellConfig piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encode(
+                        piEntity.cellId().tableEntry(), null, pipeconf))
+                .setConfig(MeterEntryCodec.getP4Config(piEntity))
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encodeKey(
+            PiMeterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(handle.cellId(), pipeconf).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encodeKey(
+            PiMeterCellConfig piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf).build();
+    }
+
+    private P4RuntimeOuterClass.DirectMeterEntry.Builder keyMsgBuilder(
+            PiMeterCellId cellId, PiPipeconf pipeconf)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encodeKey(
+                        cellId.tableEntry(), null, pipeconf));
+    }
+
+    @Override
+    protected PiMeterCellConfig decode(
+            P4RuntimeOuterClass.DirectMeterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return PiMeterCellConfig.builder()
+                .withMeterCellId(PiMeterCellId.ofDirect(
+                        CODECS.tableEntry().decode(
+                                message.getTableEntry(), null, pipeconf)))
+                .withMeterBand(new PiMeterBand(message.getConfig().getCir(),
+                                               message.getConfig().getCburst()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getPir(),
+                                               message.getConfig().getPburst()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java
new file mode 100644
index 0000000..dc10419
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime Entity.
+ */
+public final class EntityCodec extends AbstractCodec<PiEntity, P4RuntimeOuterClass.Entity, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Entity encode(
+            PiEntity piEntity, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        final P4RuntimeOuterClass.Entity.Builder p4Entity = P4RuntimeOuterClass.Entity.newBuilder();
+        switch (piEntity.piEntityType()) {
+            case TABLE_ENTRY:
+                return p4Entity.setTableEntry(
+                        CODECS.tableEntry().encode(
+                                (PiTableEntry) piEntity, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_GROUP:
+                return p4Entity.setActionProfileGroup(
+                        CODECS.actionProfileGroup().encode(
+                                (PiActionProfileGroup) piEntity, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_MEMBER:
+                return p4Entity.setActionProfileMember(
+                        CODECS.actionProfileMember().encode(
+                                (PiActionProfileMember) piEntity, null, pipeconf))
+                        .build();
+            case PRE_MULTICAST_GROUP_ENTRY:
+                return p4Entity.setPacketReplicationEngineEntry(
+                        P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                .setMulticastGroupEntry(CODECS.multicastGroupEntry().encode(
+                                        (PiMulticastGroupEntry) piEntity, null, pipeconf))
+                                .build())
+                        .build();
+            case METER_CELL_CONFIG:
+                final PiMeterCellConfig meterCellConfig = (PiMeterCellConfig) piEntity;
+                switch (meterCellConfig.cellId().meterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectMeterEntry(
+                                CODECS.directMeterEntry().encode(
+                                        meterCellConfig, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setMeterEntry(
+                                CODECS.meterEntry().encode(
+                                        meterCellConfig, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of %s of type %s is not supported",
+                                piEntity.piEntityType(),
+                                meterCellConfig.cellId().meterType()));
+                }
+            case COUNTER_CELL:
+                final PiCounterCell counterCell = (PiCounterCell) piEntity;
+                switch (counterCell.cellId().counterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectCounterEntry(
+                                CODECS.directCounterEntry().encode(
+                                        counterCell, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setCounterEntry(
+                                CODECS.counterEntry().encode(
+                                        counterCell, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of %s of type %s is not supported",
+                                piEntity.piEntityType(),
+                                counterCell.cellId().counterType()));
+                }
+            case REGISTER_CELL:
+            case PRE_CLONE_SESSION_ENTRY:
+            default:
+                throw new CodecException(format(
+                        "Encoding of %s not supported",
+                        piEntity.piEntityType()));
+        }
+    }
+
+    @Override
+    protected PiEntity decode(
+            P4RuntimeOuterClass.Entity message, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        switch (message.getEntityCase()) {
+            case TABLE_ENTRY:
+                return CODECS.tableEntry().decode(
+                        message.getTableEntry(), null, pipeconf);
+            case ACTION_PROFILE_MEMBER:
+                return CODECS.actionProfileMember().decode(
+                        message.getActionProfileMember(), null, pipeconf);
+            case ACTION_PROFILE_GROUP:
+                return CODECS.actionProfileGroup().decode(
+                        message.getActionProfileGroup(), null, pipeconf);
+            case METER_ENTRY:
+                return CODECS.meterEntry().decode(
+                        message.getMeterEntry(), null, pipeconf);
+            case DIRECT_METER_ENTRY:
+                return CODECS.directMeterEntry().decode(
+                        message.getDirectMeterEntry(), null, pipeconf);
+            case COUNTER_ENTRY:
+                return CODECS.counterEntry().decode(
+                        message.getCounterEntry(), null, pipeconf);
+            case DIRECT_COUNTER_ENTRY:
+                return CODECS.directCounterEntry().decode(
+                        message.getDirectCounterEntry(), null, pipeconf);
+            case PACKET_REPLICATION_ENGINE_ENTRY:
+                switch (message.getPacketReplicationEngineEntry().getTypeCase()) {
+                    case MULTICAST_GROUP_ENTRY:
+                        return CODECS.multicastGroupEntry().decode(
+                                message.getPacketReplicationEngineEntry()
+                                        .getMulticastGroupEntry(), null, pipeconf);
+                    case CLONE_SESSION_ENTRY:
+                    case TYPE_NOT_SET:
+                    default:
+                        throw new CodecException(format(
+                                "Decoding of %s of type %s not supported",
+                                message.getEntityCase(),
+                                message.getPacketReplicationEngineEntry().getTypeCase()));
+                }
+            case VALUE_SET_ENTRY:
+            case REGISTER_ENTRY:
+            case DIGEST_ENTRY:
+            case EXTERN_ENTRY:
+            case ENTITY_NOT_SET:
+            default:
+                throw new CodecException(format(
+                        "Decoding of %s not supported",
+                        message.getEntityCase()));
+
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java
new file mode 100644
index 0000000..f289d5d
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiMatchFieldId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiExactFieldMatch;
+import org.onosproject.net.pi.runtime.PiFieldMatch;
+import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertPrefixLen;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertSize;
+
+/**
+ * Codec for P4Runtime FieldMatch. Metadata is expected to be a Preamble for
+ * P4Info.Table.
+ */
+public final class FieldMatchCodec
+        extends AbstractCodec<PiFieldMatch, P4RuntimeOuterClass.FieldMatch,
+        P4InfoOuterClass.Preamble> {
+
+    private static final String VALUE_OF_PREFIX = "value of ";
+    private static final String MASK_OF_PREFIX = "mask of ";
+    private static final String HIGH_RANGE_VALUE_OF_PREFIX = "high range value of ";
+    private static final String LOW_RANGE_VALUE_OF_PREFIX = "low range value of ";
+
+    @Override
+    public P4RuntimeOuterClass.FieldMatch encode(
+            PiFieldMatch piFieldMatch, P4InfoOuterClass.Preamble tablePreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        P4RuntimeOuterClass.FieldMatch.Builder messageBuilder = P4RuntimeOuterClass
+                .FieldMatch.newBuilder();
+
+        // FIXME: check how field names for stacked headers are constructed in P4Runtime.
+        String fieldName = piFieldMatch.fieldId().id();
+        P4InfoOuterClass.MatchField matchFieldInfo = browser.matchFields(
+                tablePreamble.getId()).getByName(fieldName);
+        String entityName = format("field match '%s' of table '%s'",
+                                   matchFieldInfo.getName(), tablePreamble.getName());
+        int fieldId = matchFieldInfo.getId();
+        int fieldBitwidth = matchFieldInfo.getBitwidth();
+
+        messageBuilder.setFieldId(fieldId);
+
+        switch (piFieldMatch.type()) {
+            case EXACT:
+                PiExactFieldMatch fieldMatch = (PiExactFieldMatch) piFieldMatch;
+                ByteString exactValue = ByteString.copyFrom(fieldMatch.value().asReadOnlyBuffer());
+                assertSize(VALUE_OF_PREFIX + entityName, exactValue, fieldBitwidth);
+                return messageBuilder.setExact(
+                        P4RuntimeOuterClass.FieldMatch.Exact
+                                .newBuilder()
+                                .setValue(exactValue)
+                                .build())
+                        .build();
+            case TERNARY:
+                PiTernaryFieldMatch ternaryMatch = (PiTernaryFieldMatch) piFieldMatch;
+                ByteString ternaryValue = ByteString.copyFrom(ternaryMatch.value().asReadOnlyBuffer());
+                ByteString ternaryMask = ByteString.copyFrom(ternaryMatch.mask().asReadOnlyBuffer());
+                assertSize(VALUE_OF_PREFIX + entityName, ternaryValue, fieldBitwidth);
+                assertSize(MASK_OF_PREFIX + entityName, ternaryMask, fieldBitwidth);
+                return messageBuilder.setTernary(
+                        P4RuntimeOuterClass.FieldMatch.Ternary
+                                .newBuilder()
+                                .setValue(ternaryValue)
+                                .setMask(ternaryMask)
+                                .build())
+                        .build();
+            case LPM:
+                PiLpmFieldMatch lpmMatch = (PiLpmFieldMatch) piFieldMatch;
+                ByteString lpmValue = ByteString.copyFrom(lpmMatch.value().asReadOnlyBuffer());
+                int lpmPrefixLen = lpmMatch.prefixLength();
+                assertSize(VALUE_OF_PREFIX + entityName, lpmValue, fieldBitwidth);
+                assertPrefixLen(entityName, lpmPrefixLen, fieldBitwidth);
+                return messageBuilder.setLpm(
+                        P4RuntimeOuterClass.FieldMatch.LPM.newBuilder()
+                                .setValue(lpmValue)
+                                .setPrefixLen(lpmPrefixLen)
+                                .build())
+                        .build();
+            case RANGE:
+                PiRangeFieldMatch rangeMatch = (PiRangeFieldMatch) piFieldMatch;
+                ByteString rangeHighValue = ByteString.copyFrom(rangeMatch.highValue().asReadOnlyBuffer());
+                ByteString rangeLowValue = ByteString.copyFrom(rangeMatch.lowValue().asReadOnlyBuffer());
+                assertSize(HIGH_RANGE_VALUE_OF_PREFIX + entityName, rangeHighValue, fieldBitwidth);
+                assertSize(LOW_RANGE_VALUE_OF_PREFIX + entityName, rangeLowValue, fieldBitwidth);
+                return messageBuilder.setRange(
+                        P4RuntimeOuterClass.FieldMatch.Range.newBuilder()
+                                .setHigh(rangeHighValue)
+                                .setLow(rangeLowValue)
+                                .build())
+                        .build();
+            default:
+                throw new CodecException(format(
+                        "Building of match type %s not implemented", piFieldMatch.type()));
+        }
+    }
+
+    @Override
+    public PiFieldMatch decode(
+            P4RuntimeOuterClass.FieldMatch message, P4InfoOuterClass.Preamble tablePreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        String fieldMatchName = browser.matchFields(tablePreamble.getId())
+                .getById(message.getFieldId()).getName();
+        PiMatchFieldId headerFieldId = PiMatchFieldId.of(fieldMatchName);
+
+        P4RuntimeOuterClass.FieldMatch.FieldMatchTypeCase typeCase = message.getFieldMatchTypeCase();
+
+        switch (typeCase) {
+            case EXACT:
+                P4RuntimeOuterClass.FieldMatch.Exact exactFieldMatch = message.getExact();
+                ImmutableByteSequence exactValue = copyFrom(exactFieldMatch.getValue().asReadOnlyByteBuffer());
+                return new PiExactFieldMatch(headerFieldId, exactValue);
+            case TERNARY:
+                P4RuntimeOuterClass.FieldMatch.Ternary ternaryFieldMatch = message.getTernary();
+                ImmutableByteSequence ternaryValue = copyFrom(ternaryFieldMatch.getValue().asReadOnlyByteBuffer());
+                ImmutableByteSequence ternaryMask = copyFrom(ternaryFieldMatch.getMask().asReadOnlyByteBuffer());
+                return new PiTernaryFieldMatch(headerFieldId, ternaryValue, ternaryMask);
+            case LPM:
+                P4RuntimeOuterClass.FieldMatch.LPM lpmFieldMatch = message.getLpm();
+                ImmutableByteSequence lpmValue = copyFrom(lpmFieldMatch.getValue().asReadOnlyByteBuffer());
+                int lpmPrefixLen = lpmFieldMatch.getPrefixLen();
+                return new PiLpmFieldMatch(headerFieldId, lpmValue, lpmPrefixLen);
+            case RANGE:
+                P4RuntimeOuterClass.FieldMatch.Range rangeFieldMatch = message.getRange();
+                ImmutableByteSequence rangeHighValue = copyFrom(rangeFieldMatch.getHigh().asReadOnlyByteBuffer());
+                ImmutableByteSequence rangeLowValue = copyFrom(rangeFieldMatch.getLow().asReadOnlyByteBuffer());
+                return new PiRangeFieldMatch(headerFieldId, rangeLowValue, rangeHighValue);
+            default:
+                throw new CodecException(format(
+                        "Decoding of field match type '%s' not implemented", typeCase.name()));
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java
new file mode 100644
index 0000000..dc617cf
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+public final class HandleCodec extends AbstractCodec<PiHandle, P4RuntimeOuterClass.Entity, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Entity encode(
+            PiHandle piHandle, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        final P4RuntimeOuterClass.Entity.Builder p4Entity = P4RuntimeOuterClass.Entity.newBuilder();
+
+        switch (piHandle.entityType()) {
+            case TABLE_ENTRY:
+                return p4Entity.setTableEntry(
+                        CODECS.tableEntry().encodeKey(
+                                (PiTableEntryHandle) piHandle, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_GROUP:
+                return p4Entity.setActionProfileGroup(
+                        CODECS.actionProfileGroup().encodeKey(
+                                (PiActionProfileGroupHandle) piHandle, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_MEMBER:
+                return p4Entity.setActionProfileMember(
+                        CODECS.actionProfileMember().encodeKey(
+                                (PiActionProfileMemberHandle) piHandle, null, pipeconf))
+                        .build();
+            case PRE_MULTICAST_GROUP_ENTRY:
+                return p4Entity.setPacketReplicationEngineEntry(
+                        P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                .setMulticastGroupEntry(CODECS.multicastGroupEntry().encodeKey(
+                                        (PiMulticastGroupEntryHandle) piHandle, null, pipeconf))
+                                .build())
+                        .build();
+            case METER_CELL_CONFIG:
+                final PiMeterCellHandle meterCellHandle = (PiMeterCellHandle) piHandle;
+                switch (meterCellHandle.cellId().meterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectMeterEntry(
+                                CODECS.directMeterEntry().encodeKey(
+                                        meterCellHandle, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setMeterEntry(
+                                CODECS.meterEntry().encodeKey(
+                                        meterCellHandle, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of handle for %s of type %s is not supported",
+                                piHandle.entityType(),
+                                meterCellHandle.cellId().meterType()));
+                }
+            case COUNTER_CELL:
+                final PiCounterCellHandle counterCellHandle = (PiCounterCellHandle) piHandle;
+                switch (counterCellHandle.cellId().counterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectCounterEntry(
+                                CODECS.directCounterEntry().encodeKey(
+                                        counterCellHandle, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setCounterEntry(
+                                CODECS.counterEntry().encodeKey(
+                                        counterCellHandle, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of handle for %s of type %s is not supported",
+                                piHandle.entityType(),
+                                counterCellHandle.cellId().counterType()));
+                }
+            case REGISTER_CELL:
+            case PRE_CLONE_SESSION_ENTRY:
+            default:
+                throw new CodecException(format(
+                        "Encoding of handle for %s not supported",
+                        piHandle.entityType()));
+        }
+    }
+
+    @Override
+    protected PiHandle decode(
+            P4RuntimeOuterClass.Entity message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        throw new CodecException("Decoding of Entity to PiHandle is not supported");
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java
new file mode 100644
index 0000000..b210709
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiMeterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMeterBand;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+/**
+ * Codec for P4Runtime MeterEntry.
+ */
+public final class MeterEntryCodec
+        extends AbstractEntityCodec<PiMeterCellConfig, PiMeterCellHandle,
+        P4RuntimeOuterClass.MeterEntry, Object> {
+
+    static P4RuntimeOuterClass.MeterConfig getP4Config(PiMeterCellConfig piConfig)
+            throws CodecException {
+        if (piConfig.meterBands().size() != 2) {
+            throw new CodecException("Number of meter bands should be 2");
+        }
+        final PiMeterBand[] bands = piConfig.meterBands().toArray(new PiMeterBand[0]);
+        long cir, cburst, pir, pburst;
+        // The band with bigger burst is peak if rate of them is equal.
+        if (bands[0].rate() > bands[1].rate() ||
+                (bands[0].rate() == bands[1].rate() &&
+                        bands[0].burst() >= bands[1].burst())) {
+            cir = bands[1].rate();
+            cburst = bands[1].burst();
+            pir = bands[0].rate();
+            pburst = bands[0].burst();
+        } else {
+            cir = bands[0].rate();
+            cburst = bands[0].burst();
+            pir = bands[1].rate();
+            pburst = bands[1].burst();
+        }
+        return P4RuntimeOuterClass.MeterConfig.newBuilder()
+                .setCir(cir)
+                .setCburst(cburst)
+                .setPir(pir)
+                .setPburst(pburst)
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encode(
+            PiMeterCellConfig piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final int meterId = browser.meters().getByName(
+                piEntity.cellId().meterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.MeterEntry.newBuilder()
+                .setMeterId(meterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(piEntity.cellId().index()).build())
+                .setConfig(getP4Config(piEntity))
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encodeKey(
+            PiMeterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.cellId(), browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encodeKey(
+            PiMeterCellConfig piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser).build();
+    }
+
+    private P4RuntimeOuterClass.MeterEntry.Builder keyMsgBuilder(
+            PiMeterCellId cellId, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int meterId = browser.meters().getByName(
+                cellId.meterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.MeterEntry.newBuilder()
+                .setMeterId(meterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(cellId.index()).build());
+    }
+
+    @Override
+    protected PiMeterCellConfig decode(
+            P4RuntimeOuterClass.MeterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String meterName = browser.meters()
+                .getById(message.getMeterId())
+                .getPreamble()
+                .getName();
+        return PiMeterCellConfig.builder()
+                .withMeterCellId(PiMeterCellId.ofIndirect(
+                        PiMeterId.of(meterName), message.getIndex().getIndex()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getCir(),
+                                               message.getConfig().getCburst()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getPir(),
+                                               message.getConfig().getPburst()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java
new file mode 100644
index 0000000..38080cc
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.Replica;
+
+import static java.lang.String.format;
+
+/**
+ * Codec for P4Runtime MulticastGroupEntry.
+ */
+public final class MulticastGroupEntryCodec
+        extends AbstractEntityCodec<PiMulticastGroupEntry, PiMulticastGroupEntryHandle,
+        P4RuntimeOuterClass.MulticastGroupEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encode(
+            PiMulticastGroupEntry piEntity, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) throws CodecException {
+        final P4RuntimeOuterClass.MulticastGroupEntry.Builder msgBuilder =
+                P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                        .setMulticastGroupId(piEntity.groupId());
+        for (PiPreReplica replica : piEntity.replicas()) {
+            final int p4PortId;
+            try {
+                p4PortId = Math.toIntExact(replica.egressPort().toLong());
+            } catch (ArithmeticException e) {
+                throw new CodecException(format(
+                        "Cannot cast 64 bit port value '%s' to 32 bit",
+                        replica.egressPort()));
+            }
+            msgBuilder.addReplicas(
+                    Replica.newBuilder()
+                            .setEgressPort(p4PortId)
+                            .setInstance(replica.instanceId())
+                            .build());
+        }
+        return msgBuilder.build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encodeKey(
+            PiMulticastGroupEntryHandle handle, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                .setMulticastGroupId(handle.groupId()).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encodeKey(
+            PiMulticastGroupEntry piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                .setMulticastGroupId(piEntity.groupId()).build();
+    }
+
+    @Override
+    protected PiMulticastGroupEntry decode(
+            P4RuntimeOuterClass.MulticastGroupEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        final PiMulticastGroupEntry.Builder piEntryBuilder = PiMulticastGroupEntry.builder();
+        piEntryBuilder.withGroupId(message.getMulticastGroupId());
+        message.getReplicasList().stream()
+                .map(r -> new PiPreReplica(
+                        PortNumber.portNumber(r.getEgressPort()), r.getInstance()))
+                .forEach(piEntryBuilder::addReplica);
+        return piEntryBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4DataCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java
similarity index 94%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4DataCodec.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java
index 551bf5c..d55b614 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4DataCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.codec;
 
 import com.google.protobuf.ByteString;
 import org.onlab.util.ImmutableByteSequence;
@@ -41,8 +41,10 @@
 import static p4.v1.P4DataOuterClass.P4StructLike;
 
 /**
- * Encoder/decoder of PI Data entry to P4 Data entry protobuf
- * messages, and vice versa.
+ * Encoder/decoder of PI Data entry to P4 Data entry protobuf messages, and vice
+ * versa.
+ * <p>
+ * TODO: implement codec for each P4Data type using AbstractP4RuntimeCodec.
  */
 final class P4DataCodec {
 
@@ -145,7 +147,7 @@
                 builder.setHeader(encodeHeader((PiHeader) piData));
                 break;
             case HEADERSTACK:
-                P4HeaderStack.Builder headerStack =  P4HeaderStack.newBuilder();
+                P4HeaderStack.Builder headerStack = P4HeaderStack.newBuilder();
                 int i = 0;
                 for (PiHeader header : ((PiHeaderStack) piData).headers()) {
                     headerStack.setEntries(i, encodeHeader(header));
@@ -157,7 +159,7 @@
                 builder.setHeaderUnion(encodeHeaderUnion((PiHeaderUnion) piData));
                 break;
             case HEADERUNIONSTACK:
-                P4HeaderUnionStack.Builder headerUnionStack =  P4HeaderUnionStack.newBuilder();
+                P4HeaderUnionStack.Builder headerUnionStack = P4HeaderUnionStack.newBuilder();
                 int j = 0;
                 for (PiHeaderUnion headerUnion : ((PiHeaderUnionStack) piData).headerUnions()) {
                     headerUnionStack.setEntries(j, encodeHeaderUnion(headerUnion));
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java
new file mode 100644
index 0000000..e4de896
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPacketOperationType;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime PacketIn. Only decoding is supported, as encoding is not
+ * meaningful in this case (packet-ins are always generated by the server).
+ */
+public final class PacketInCodec
+        extends AbstractCodec<PiPacketOperation,
+        P4RuntimeOuterClass.PacketIn, Object> {
+
+    private static final String PACKET_IN = "packet_in";
+
+    @Override
+    protected P4RuntimeOuterClass.PacketIn encode(
+            PiPacketOperation piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        throw new CodecException("Encoding of packet-in is not supported");
+    }
+
+    @Override
+    protected PiPacketOperation decode(
+            P4RuntimeOuterClass.PacketIn message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4InfoOuterClass.Preamble ctrlPktMetaPreamble = browser
+                .controllerPacketMetadatas()
+                .getByName(PACKET_IN)
+                .getPreamble();
+        return PiPacketOperation.builder()
+                .withType(PiPacketOperationType.PACKET_IN)
+                .withMetadatas(CODECS.packetMetadata().decodeAll(
+                        message.getMetadataList(), ctrlPktMetaPreamble, pipeconf))
+                .withData(copyFrom(message.getPayload().asReadOnlyByteBuffer()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java
new file mode 100644
index 0000000..3f78085
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+
+/**
+ * Coded for P4Runtime PacketMetadata. The metadata is expected to be a Preamble
+ * of a P4Info.ControllerPacketMetadata message.
+ */
+public final class PacketMetadataCodec
+        extends AbstractCodec<PiPacketMetadata,
+        P4RuntimeOuterClass.PacketMetadata, P4InfoOuterClass.Preamble> {
+
+    @Override
+    protected P4RuntimeOuterClass.PacketMetadata encode(
+            PiPacketMetadata piEntity, P4InfoOuterClass.Preamble ctrlPktMetaPreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int metadataId = browser
+                .packetMetadatas(ctrlPktMetaPreamble.getId())
+                .getByName(piEntity.id().id()).getId();
+        return P4RuntimeOuterClass.PacketMetadata.newBuilder()
+                .setMetadataId(metadataId)
+                .setValue(ByteString.copyFrom(piEntity.value().asReadOnlyBuffer()))
+                .build();
+    }
+
+    @Override
+    protected PiPacketMetadata decode(
+            P4RuntimeOuterClass.PacketMetadata message,
+            P4InfoOuterClass.Preamble ctrlPktMetaPreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String packetMetadataName = browser
+                .packetMetadatas(ctrlPktMetaPreamble.getId())
+                .getById(message.getMetadataId()).getName();
+        final PiPacketMetadataId metadataId = PiPacketMetadataId
+                .of(packetMetadataName);
+        return PiPacketMetadata.builder()
+                .withId(metadataId)
+                .withValue(copyFrom(message.getValue().asReadOnlyByteBuffer()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java
new file mode 100644
index 0000000..6d020c3
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime PacketOut. Only encoding is supported, as decoding is not
+ * meaningful in this case (packet-outs are always generated by the client).
+ */
+public final class PacketOutCodec
+        extends AbstractCodec<PiPacketOperation,
+        P4RuntimeOuterClass.PacketOut, Object> {
+
+    private static final String PACKET_OUT = "packet_out";
+
+    @Override
+    protected P4RuntimeOuterClass.PacketOut encode(
+            PiPacketOperation piPacket, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4InfoOuterClass.Preamble ctrlPktMetaPreamble = browser
+                .controllerPacketMetadatas()
+                .getByName(PACKET_OUT)
+                .getPreamble();
+        return P4RuntimeOuterClass.PacketOut.newBuilder()
+                .addAllMetadata(CODECS.packetMetadata().encodeAll(
+                        piPacket.metadatas(), ctrlPktMetaPreamble, pipeconf))
+                .setPayload(ByteString.copyFrom(piPacket.data().asReadOnlyBuffer()))
+                .build();
+
+    }
+
+    @Override
+    protected PiPacketOperation decode(
+            P4RuntimeOuterClass.PacketOut message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        throw new CodecException("Decoding of packet-out is not supported");
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java
new file mode 100644
index 0000000..f3f309b
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiMatchKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.OptionalInt;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime TableEntry.
+ */
+public final class TableEntryCodec
+        extends AbstractEntityCodec<PiTableEntry, PiTableEntryHandle,
+        P4RuntimeOuterClass.TableEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encode(
+            PiTableEntry piTableEntry, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4RuntimeOuterClass.TableEntry.Builder tableEntryMsgBuilder =
+                keyMsgBuilder(piTableEntry.table(), piTableEntry.matchKey(),
+                              piTableEntry.priority(), pipeconf, browser);
+        // Controller metadata (cookie)
+        tableEntryMsgBuilder.setControllerMetadata(piTableEntry.cookie());
+        // Timeout.
+        if (piTableEntry.timeout().isPresent()) {
+            // FIXME: timeout is supported in P4Runtime v1.0
+            log.warn("Found PI table entry with timeout set, " +
+                             "not supported in P4Runtime: {}", piTableEntry);
+        }
+        // Table action.
+        if (piTableEntry.action() != null) {
+            tableEntryMsgBuilder.setAction(
+                    encodePiTableAction(piTableEntry.action(), pipeconf));
+        }
+        // Counter.
+        if (piTableEntry.counter() != null) {
+            tableEntryMsgBuilder.setCounterData(encodeCounter(piTableEntry.counter()));
+        }
+        return tableEntryMsgBuilder.build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encodeKey(
+            PiTableEntryHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser) throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.tableId(), handle.matchKey(),
+                             handle.priority(), pipeconf, browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encodeKey(
+            PiTableEntry piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser) throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.table(), piEntity.matchKey(),
+                             piEntity.priority(), pipeconf, browser).build();
+    }
+
+    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+    private P4RuntimeOuterClass.TableEntry.Builder keyMsgBuilder(
+            PiTableId tableId, PiMatchKey matchKey, OptionalInt priority,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final P4RuntimeOuterClass.TableEntry.Builder tableEntryMsgBuilder =
+                P4RuntimeOuterClass.TableEntry.newBuilder();
+        final P4InfoOuterClass.Preamble tablePreamble = browser.tables()
+                .getByName(tableId.id()).getPreamble();
+        // Table id.
+        tableEntryMsgBuilder.setTableId(tablePreamble.getId());
+        // Field matches.
+        if (matchKey.equals(PiMatchKey.EMPTY)) {
+            tableEntryMsgBuilder.setIsDefaultAction(true);
+        } else {
+            tableEntryMsgBuilder.addAllMatch(
+                    CODECS.fieldMatch().encodeAll(
+                            matchKey.fieldMatches(),
+                            tablePreamble, pipeconf));
+        }
+        // Priority.
+        priority.ifPresent(tableEntryMsgBuilder::setPriority);
+        return tableEntryMsgBuilder;
+    }
+
+    @Override
+    protected PiTableEntry decode(
+            P4RuntimeOuterClass.TableEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        PiTableEntry.Builder piTableEntryBuilder = PiTableEntry.builder();
+
+        P4InfoOuterClass.Preamble tablePreamble = browser.tables()
+                .getById(message.getTableId()).getPreamble();
+
+        // Table id.
+        piTableEntryBuilder.forTable(PiTableId.of(tablePreamble.getName()));
+
+        // Priority.
+        if (message.getPriority() > 0) {
+            piTableEntryBuilder.withPriority(message.getPriority());
+        }
+
+        // Controller metadata (cookie)
+        piTableEntryBuilder.withCookie(message.getControllerMetadata());
+
+        // Table action.
+        if (message.hasAction()) {
+            piTableEntryBuilder.withAction(decodeTableActionMsg(
+                    message.getAction(), pipeconf));
+        }
+
+        // Timeout.
+        // FIXME: how to decode table entry messages with timeout, given that
+        //  the timeout value is lost after encoding?
+
+        // Match key for field matches.
+        piTableEntryBuilder.withMatchKey(
+                PiMatchKey.builder()
+                        .addFieldMatches(CODECS.fieldMatch().decodeAll(
+                                message.getMatchList(),
+                                tablePreamble, pipeconf))
+                        .build());
+
+        // Counter.
+        piTableEntryBuilder.withCounterCellData(decodeCounter(message.getCounterData()));
+
+        return piTableEntryBuilder.build();
+    }
+
+    private P4RuntimeOuterClass.TableAction encodePiTableAction(
+            PiTableAction piTableAction, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(piTableAction, "Cannot encode null PiTableAction");
+        final P4RuntimeOuterClass.TableAction.Builder tableActionMsgBuilder =
+                P4RuntimeOuterClass.TableAction.newBuilder();
+        switch (piTableAction.type()) {
+            case ACTION:
+                P4RuntimeOuterClass.Action theAction = CODECS.action()
+                        .encode((PiAction) piTableAction, null, pipeconf);
+                tableActionMsgBuilder.setAction(theAction);
+                break;
+            case ACTION_PROFILE_GROUP_ID:
+                tableActionMsgBuilder.setActionProfileGroupId(
+                        ((PiActionProfileGroupId) piTableAction).id());
+                break;
+            case ACTION_PROFILE_MEMBER_ID:
+                tableActionMsgBuilder.setActionProfileMemberId(
+                        ((PiActionProfileMemberId) piTableAction).id());
+                break;
+            default:
+                throw new CodecException(
+                        format("Building of table action type %s not implemented",
+                               piTableAction.type()));
+        }
+        return tableActionMsgBuilder.build();
+    }
+
+    private PiTableAction decodeTableActionMsg(
+            P4RuntimeOuterClass.TableAction tableActionMsg, PiPipeconf pipeconf)
+            throws CodecException {
+        P4RuntimeOuterClass.TableAction.TypeCase typeCase = tableActionMsg.getTypeCase();
+        switch (typeCase) {
+            case ACTION:
+                P4RuntimeOuterClass.Action actionMsg = tableActionMsg.getAction();
+                return CODECS.action().decode(
+                        actionMsg, null, pipeconf);
+            case ACTION_PROFILE_GROUP_ID:
+                return PiActionProfileGroupId.of(
+                        tableActionMsg.getActionProfileGroupId());
+            case ACTION_PROFILE_MEMBER_ID:
+                return PiActionProfileMemberId.of(
+                        tableActionMsg.getActionProfileMemberId());
+            default:
+                throw new CodecException(
+                        format("Decoding of table action type %s not implemented",
+                               typeCase.name()));
+        }
+    }
+
+    private P4RuntimeOuterClass.CounterData encodeCounter(
+            PiCounterCellData piCounterCellData) {
+        return P4RuntimeOuterClass.CounterData.newBuilder()
+                .setPacketCount(piCounterCellData.packets())
+                .setByteCount(piCounterCellData.bytes()).build();
+    }
+
+    private PiCounterCellData decodeCounter(
+            P4RuntimeOuterClass.CounterData counterData) {
+        return new PiCounterCellData(
+                counterData.getPacketCount(), counterData.getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java
similarity index 79%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java
index 604b1f0..c72489a 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,19 +14,18 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.codec;
 
 import com.google.protobuf.ByteString;
-import p4.v1.P4RuntimeOuterClass;
 
 import static java.lang.String.format;
 
 /**
- * Utilities for P4 runtime control.
+ * Codec utilities.
  */
-final class P4RuntimeUtils {
+final class Utils {
 
-    private P4RuntimeUtils() {
+    private Utils() {
         // Hide default construction
     }
 
@@ -50,8 +49,4 @@
                     entityDescr, bitWidth, prefixLength));
         }
     }
-
-    static P4RuntimeOuterClass.Index indexMsg(long index) {
-        return P4RuntimeOuterClass.Index.newBuilder().setIndex(index).build();
-    }
 }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java
similarity index 62%
copy from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
copy to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java
index 89d5510..25cee2b 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java
@@ -14,18 +14,8 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
-
 /**
- * Signals an error during encoding/decoding of a PI entity/protobuf message.
+ * Classes to translates from PI framework-related objects to P4Runtime protobuf
+ * messages, and vice versa.
  */
-public final class CodecException extends Exception {
-
-    /**
-     * Ceeates anew exception with the given explanation message.
-     * @param explanation explanation
-     */
-    public CodecException(String explanation) {
-        super(explanation);
-    }
-}
+package org.onosproject.p4runtime.ctl.codec;
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ArbitrationResponse.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ArbitrationUpdateEvent.java
similarity index 83%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ArbitrationResponse.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ArbitrationUpdateEvent.java
index f93a4cd..80e3e98 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ArbitrationResponse.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ArbitrationUpdateEvent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import org.onosproject.net.DeviceId;
 import org.onosproject.p4runtime.api.P4RuntimeEventSubject;
@@ -22,7 +22,7 @@
 /**
  * Default implementation of arbitration in P4Runtime.
  */
-final class ArbitrationResponse implements P4RuntimeEventSubject {
+public final class ArbitrationUpdateEvent implements P4RuntimeEventSubject {
 
     private DeviceId deviceId;
     private boolean isMaster;
@@ -33,7 +33,7 @@
      * @param deviceId the device
      * @param isMaster true if arbitration response signals master status
      */
-    ArbitrationResponse(DeviceId deviceId, boolean isMaster) {
+    public ArbitrationUpdateEvent(DeviceId deviceId, boolean isMaster) {
         this.deviceId = deviceId;
         this.isMaster = isMaster;
     }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/BaseP4RuntimeEventSubject.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/BaseEventSubject.java
similarity index 81%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/BaseP4RuntimeEventSubject.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/BaseEventSubject.java
index a78f10c..089d934 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/BaseP4RuntimeEventSubject.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/BaseEventSubject.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import org.onosproject.net.DeviceId;
 import org.onosproject.p4runtime.api.P4RuntimeEventSubject;
@@ -23,7 +23,7 @@
  * Base P4Runtime event subject that carries just the device ID that originated
  * the event.
  */
-final class BaseP4RuntimeEventSubject implements P4RuntimeEventSubject {
+public final class BaseEventSubject implements P4RuntimeEventSubject {
 
     private DeviceId deviceId;
 
@@ -32,7 +32,7 @@
      *
      * @param deviceId the device
      */
-    BaseP4RuntimeEventSubject(DeviceId deviceId) {
+    public BaseEventSubject(DeviceId deviceId) {
         this.deviceId = deviceId;
     }
 
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ChannelEvent.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ChannelEvent.java
similarity index 83%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ChannelEvent.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ChannelEvent.java
index 6e33514..0a77e46 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ChannelEvent.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/ChannelEvent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import org.onosproject.net.DeviceId;
 import org.onosproject.p4runtime.api.P4RuntimeEventSubject;
@@ -22,9 +22,9 @@
 /**
  * Channel event in P4Runtime.
  */
-final class ChannelEvent implements P4RuntimeEventSubject {
+public final class ChannelEvent implements P4RuntimeEventSubject {
 
-    enum Type {
+    public enum Type {
         OPEN,
         CLOSED,
         ERROR
@@ -39,7 +39,7 @@
      * @param deviceId  the device
      * @param type      error type
      */
-    ChannelEvent(DeviceId deviceId, Type type) {
+    public ChannelEvent(DeviceId deviceId, Type type) {
         this.deviceId = deviceId;
         this.type = type;
     }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/DistributedElectionIdGenerator.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/DistributedElectionIdGenerator.java
similarity index 94%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/DistributedElectionIdGenerator.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/DistributedElectionIdGenerator.java
index 75c068e..980ab11 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/DistributedElectionIdGenerator.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/DistributedElectionIdGenerator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import org.onlab.util.KryoNamespace;
 import org.onosproject.net.DeviceId;
@@ -34,7 +34,7 @@
 /**
  * Distributed implementation of a generator of P4Runtime election IDs.
  */
-class DistributedElectionIdGenerator {
+final class DistributedElectionIdGenerator {
 
     private final Logger log = getLogger(this.getClass());
 
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeControllerImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/P4RuntimeControllerImpl.java
similarity index 90%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeControllerImpl.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/P4RuntimeControllerImpl.java
index ac1d90f..affbf7d 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeControllerImpl.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/P4RuntimeControllerImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import com.google.common.collect.Maps;
 import io.grpc.ManagedChannel;
@@ -22,12 +22,14 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.device.DeviceAgentEvent;
 import org.onosproject.net.device.DeviceAgentListener;
+import org.onosproject.net.pi.service.PiPipeconfService;
 import org.onosproject.net.provider.ProviderId;
 import org.onosproject.p4runtime.api.P4RuntimeClient;
 import org.onosproject.p4runtime.api.P4RuntimeClientKey;
 import org.onosproject.p4runtime.api.P4RuntimeController;
 import org.onosproject.p4runtime.api.P4RuntimeEvent;
 import org.onosproject.p4runtime.api.P4RuntimeEventListener;
+import org.onosproject.p4runtime.ctl.client.P4RuntimeClientImpl;
 import org.onosproject.store.service.StorageService;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -61,6 +63,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
     private StorageService storageService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private PiPipeconfService pipeconfService;
+
     @Activate
     public void activate() {
         super.activate();
@@ -80,7 +85,7 @@
 
     @Override
     protected P4RuntimeClient createClientInstance(P4RuntimeClientKey clientKey, ManagedChannel channel) {
-        return new P4RuntimeClientImpl(clientKey, channel, this);
+        return new P4RuntimeClientImpl(clientKey, channel, this, pipeconfService);
     }
 
     @Override
@@ -102,11 +107,11 @@
         });
     }
 
-    BigInteger newMasterElectionId(DeviceId deviceId) {
+    public BigInteger newMasterElectionId(DeviceId deviceId) {
         return electionIdGenerator.generate(deviceId);
     }
 
-    void postEvent(P4RuntimeEvent event) {
+    public void postEvent(P4RuntimeEvent event) {
         switch (event.type()) {
             case CHANNEL_EVENT:
                 handleChannelEvent(event);
@@ -153,7 +158,7 @@
 
     private void handleArbitrationReply(P4RuntimeEvent event) {
         final DeviceId deviceId = event.subject().deviceId();
-        final ArbitrationResponse response = (ArbitrationResponse) event.subject();
+        final ArbitrationUpdateEvent response = (ArbitrationUpdateEvent) event.subject();
         final DeviceAgentEvent.Type roleType = response.isMaster()
                 ? DeviceAgentEvent.Type.ROLE_MASTER
                 : DeviceAgentEvent.Type.ROLE_STANDBY;
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketInEvent.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/PacketInEvent.java
similarity index 88%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketInEvent.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/PacketInEvent.java
index dddee2b..4a983a8 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PacketInEvent.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/PacketInEvent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.controller;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
@@ -27,12 +27,12 @@
 /**
  * P4Runtime packet-in.
  */
-final class PacketInEvent implements P4RuntimePacketIn {
+public final class PacketInEvent implements P4RuntimePacketIn {
 
     private final DeviceId deviceId;
     private final PiPacketOperation operation;
 
-    PacketInEvent(DeviceId deviceId, PiPacketOperation operation) {
+    public PacketInEvent(DeviceId deviceId, PiPacketOperation operation) {
         this.deviceId = checkNotNull(deviceId);
         this.operation = checkNotNull(operation);
     }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/package-info.java
similarity index 62%
copy from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
copy to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/package-info.java
index 89d5510..4d37da9 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/controller/package-info.java
@@ -14,18 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
-
 /**
- * Signals an error during encoding/decoding of a PI entity/protobuf message.
+ * P4Runtime controller implementation classes.
  */
-public final class CodecException extends Exception {
-
-    /**
-     * Ceeates anew exception with the given explanation message.
-     * @param explanation explanation
-     */
-    public CodecException(String explanation) {
-        super(explanation);
-    }
-}
+package org.onosproject.p4runtime.ctl.controller;
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
similarity index 87%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
index 801ce36..29cddff 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4InfoBrowser.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.p4runtime.ctl;
+package org.onosproject.p4runtime.ctl.utils;
 
 
 import com.google.common.collect.Maps;
@@ -40,7 +40,7 @@
 /**
  * Utility class to easily retrieve information from a P4Info protobuf message.
  */
-final class P4InfoBrowser {
+public final class P4InfoBrowser {
 
     private final EntityBrowser<Table> tables = new EntityBrowser<>("table");
     private final EntityBrowser<Action> actions = new EntityBrowser<>("action");
@@ -61,7 +61,7 @@
      *
      * @param p4info P4Info protobuf message
      */
-    P4InfoBrowser(P4Info p4info) {
+    public P4InfoBrowser(P4Info p4info) {
         parseP4Info(p4info);
     }
 
@@ -123,7 +123,7 @@
      *
      * @return table browser
      */
-    EntityBrowser<Table> tables() {
+    public EntityBrowser<Table> tables() {
         return tables;
     }
 
@@ -132,7 +132,7 @@
      *
      * @return action browser
      */
-    EntityBrowser<Action> actions() {
+    public EntityBrowser<Action> actions() {
         return actions;
     }
 
@@ -141,7 +141,7 @@
      *
      * @return action profile browser
      */
-    EntityBrowser<ActionProfile> actionProfiles() {
+    public EntityBrowser<ActionProfile> actionProfiles() {
         return actionProfiles;
     }
 
@@ -150,7 +150,7 @@
      *
      * @return counter browser
      */
-    EntityBrowser<Counter> counters() {
+    public EntityBrowser<Counter> counters() {
         return counters;
     }
 
@@ -159,7 +159,7 @@
      *
      * @return direct counter browser
      */
-    EntityBrowser<DirectCounter> directCounters() {
+    public EntityBrowser<DirectCounter> directCounters() {
         return directCounters;
     }
 
@@ -168,7 +168,7 @@
      *
      * @return meter browser
      */
-    EntityBrowser<Meter> meters() {
+    public EntityBrowser<Meter> meters() {
         return meters;
     }
 
@@ -177,7 +177,7 @@
      *
      * @return table browser
      */
-    EntityBrowser<DirectMeter> directMeters() {
+    public EntityBrowser<DirectMeter> directMeters() {
         return directMeters;
     }
 
@@ -186,7 +186,7 @@
      *
      * @return controller packet metadata browser
      */
-    EntityBrowser<ControllerPacketMetadata> controllerPacketMetadatas() {
+    public EntityBrowser<ControllerPacketMetadata> controllerPacketMetadatas() {
         return ctrlPktMetadatas;
     }
 
@@ -197,7 +197,7 @@
      * @return action params browser
      * @throws NotFoundException if the action cannot be found
      */
-    EntityBrowser<Action.Param> actionParams(int actionId) throws NotFoundException {
+    public EntityBrowser<Action.Param> actionParams(int actionId) throws NotFoundException {
         // Throws exception if action id is not found.
         actions.getById(actionId);
         return actionParams.get(actionId);
@@ -210,7 +210,7 @@
      * @return match field browser
      * @throws NotFoundException if the table cannot be found
      */
-    EntityBrowser<MatchField> matchFields(int tableId) throws NotFoundException {
+    public EntityBrowser<MatchField> matchFields(int tableId) throws NotFoundException {
         // Throws exception if action id is not found.
         tables.getById(tableId);
         return matchFields.get(tableId);
@@ -223,7 +223,7 @@
      * @return metadata browser
      * @throws NotFoundException controller packet metadata cannot be found
      */
-    EntityBrowser<ControllerPacketMetadata.Metadata> packetMetadatas(int controllerPacketMetadataId)
+    public EntityBrowser<ControllerPacketMetadata.Metadata> packetMetadatas(int controllerPacketMetadataId)
             throws NotFoundException {
         // Throws exception if controller packet metadata id is not found.
         ctrlPktMetadatas.getById(controllerPacketMetadataId);
@@ -235,7 +235,7 @@
      *
      * @param <T> protobuf message type
      */
-    static final class EntityBrowser<T extends Message> {
+    public static final class EntityBrowser<T extends Message> {
 
         private String entityName;
         private final Map<String, T> names = Maps.newHashMap();
@@ -254,7 +254,7 @@
          * @param id     entity id
          * @param entity entity message
          */
-        void add(String name, String alias, int id, T entity) {
+        private void add(String name, String alias, int id, T entity) {
             checkNotNull(name);
             checkArgument(!name.isEmpty(), "Name cannot be empty");
             checkNotNull(entity);
@@ -271,7 +271,7 @@
          * @param preamble P4Info preamble protobuf message
          * @param entity   entity message
          */
-        void addWithPreamble(Preamble preamble, T entity) {
+        private void addWithPreamble(Preamble preamble, T entity) {
             checkNotNull(preamble);
             add(preamble.getName(), preamble.getAlias(), preamble.getId(), entity);
         }
@@ -282,7 +282,7 @@
          * @param name entity name
          * @return boolean
          */
-        boolean hasName(String name) {
+        public boolean hasName(String name) {
             return names.containsKey(name);
         }
 
@@ -293,7 +293,7 @@
          * @return entity message
          * @throws NotFoundException if the entity cannot be found
          */
-        T getByName(String name) throws NotFoundException {
+        public T getByName(String name) throws NotFoundException {
             if (hasName(name)) {
                 return names.get(name);
             } else {
@@ -311,7 +311,7 @@
          * @param id entity id
          * @return boolean
          */
-        boolean hasId(int id) {
+        public boolean hasId(int id) {
             return ids.containsKey(id);
         }
 
@@ -322,7 +322,7 @@
          * @return entity message
          * @throws NotFoundException if the entity cannot be found
          */
-        T getById(int id) throws NotFoundException {
+        public T getById(int id) throws NotFoundException {
             if (!hasId(id)) {
                 throw new NotFoundException(entityName, id);
             }
@@ -335,12 +335,13 @@
      */
     public static final class NotFoundException extends Exception {
 
-        NotFoundException(String entityName, String key, String hint) {
+        public NotFoundException(String entityName, String key, String hint) {
             super(format(
-                    "No such %s in P4Info with name '%s'%s", entityName, key, hint.isEmpty() ? "" : " (" + hint + ")"));
+                    "No such %s in P4Info with name '%s'%s",
+                    entityName, key, hint.isEmpty() ? "" : " (" + hint + ")"));
         }
 
-        NotFoundException(String entityName, int id) {
+        public NotFoundException(String entityName, int id) {
             super(format("No such %s in P4Info with id '%d'", entityName, id));
         }
     }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PipeconfHelper.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
similarity index 91%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PipeconfHelper.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
index 85fa2a8..dec7438 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/PipeconfHelper.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation