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/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
+ * 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.cache.Cache;
 import com.google.common.cache.CacheBuilder;
@@ -37,9 +37,9 @@
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
- * Utility class to deal with pipeconfs in the context of P4runtime.
+ * Utility class to deal with pipeconfs in the context of P4Runtime.
  */
-final class PipeconfHelper {
+public final class PipeconfHelper {
 
     private static final int P4INFO_BROWSER_EXPIRE_TIME_IN_MIN = 10;
     private static final Logger log = getLogger(PipeconfHelper.class);
@@ -60,7 +60,7 @@
      * @param pipeconf pipeconf
      * @return P4Info or null
      */
-    static P4Info getP4Info(PiPipeconf pipeconf) {
+    public static P4Info getP4Info(PiPipeconf pipeconf) {
         return P4INFOS.computeIfAbsent(pipeconf.id(), piPipeconfId -> {
             if (!pipeconf.extension(P4_INFO_TEXT).isPresent()) {
                 log.warn("Missing P4Info extension in pipeconf {}", pipeconf.id());
@@ -88,7 +88,7 @@
      * @param pipeconf pipeconf
      * @return P4Info browser or null
      */
-    static P4InfoBrowser getP4InfoBrowser(PiPipeconf pipeconf) {
+    public static P4InfoBrowser getP4InfoBrowser(PiPipeconf pipeconf) {
         try {
             return BROWSERS.get(pipeconf.id(), () -> {
                 P4Info p4info = PipeconfHelper.getP4Info(pipeconf);
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/utils/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/utils/package-info.java
index 89d5510..ae09455 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/utils/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.
+ * Utility classes for the P4Runtime protocol subsystem.
  */
-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.utils;