ONOS-7001 Support for direct counters

Currently Bmv2 returns UNKNOWN error when reading direct counters.

Change-Id: I834d7b5a8627181c6888500545e1bdbfe9af8dc1
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
index 6b93f5d..738996f 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
@@ -24,6 +24,7 @@
 import org.onosproject.net.packet.InboundPacket;
 import org.onosproject.net.packet.OutboundPacket;
 import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiCounterId;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiTableId;
@@ -88,6 +89,15 @@
             throws PiInterpreterException;
 
     /**
+     * Returns a protocol-independent direct counter identifier for the given table, if present. If not present, it
+     * means that the given table does not support direct counters.
+     *
+     * @param piTableId table identifier
+     * @return optional direct counter identifier
+     */
+    Optional<PiCounterId> mapTableCounter(PiTableId piTableId);
+
+    /**
      * Returns a collection of packet operations equivalent to the given OutboundPacket.
      *
      * @param packet a ONOS outbound packet
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellId.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellId.java
index d762f23..cd88d2a 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellId.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterCellId.java
@@ -16,77 +16,22 @@
 
 package org.onosproject.net.pi.runtime;
 
-import com.google.common.annotations.Beta;
-import com.google.common.base.Objects;
-import org.onlab.util.Identifier;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 /**
- * Identifier of a counter cell of a protocol-independent pipeline.
+ * Identifier of a counter cell in a protocol-independent pipeline.
  */
-@Beta
-public final class PiCounterCellId extends Identifier<String> {
-
-    private final PiCounterId counterId;
-    private final long index;
-
-    private PiCounterCellId(PiCounterId counterId, long index) {
-        super(counterId.id() + "[" + index + "]");
-        this.counterId = counterId;
-        this.index = index;
-    }
+public interface PiCounterCellId {
 
     /**
-     * Returns a counter cell identifier for the given counter identifier and index.
-     *
-     * @param counterId counter identifier
-     * @param index     index
-     * @return counter cell identifier
-     */
-    public static PiCounterCellId of(PiCounterId counterId, long index) {
-        checkNotNull(counterId);
-        checkArgument(index >= 0, "Index must be a positive integer");
-        return new PiCounterCellId(counterId, index);
-    }
-
-    /**
-     * Returns the counter identifier of this cell.
+     * Returns the identifier of the counter instance where this cell is contained.
      *
      * @return counter identifier
      */
-    public PiCounterId counterId() {
-        return counterId;
-    }
+    PiCounterId counterId();
 
     /**
-     * Returns the index of this cell.
+     * Returns the type of counter identified.
      *
-     * @return cell index
+     * @return counter type
      */
-    public long index() {
-        return index;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof PiCounterCellId)) {
-            return false;
-        }
-        if (!super.equals(o)) {
-            return false;
-        }
-        PiCounterCellId that = (PiCounterCellId) o;
-        return index == that.index &&
-                Objects.equal(counterId, that.counterId);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(super.hashCode(), counterId, index);
-    }
+    PiCounterType type();
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterId.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterId.java
index 6fcd55e..dcc7c02 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterId.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterId.java
@@ -17,7 +17,7 @@
 package org.onosproject.net.pi.runtime;
 
 import com.google.common.annotations.Beta;
-import org.onlab.util.Identifier;
+import com.google.common.base.Objects;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -26,22 +26,28 @@
  * Identifier of a counter of a protocol-independent pipeline.
  */
 @Beta
-public final class PiCounterId extends Identifier<String> {
+public final class PiCounterId {
 
-    private PiCounterId(String name) {
-        super(name);
+    private final String name;
+    private final PiCounterType type;
+
+    private PiCounterId(String name, PiCounterType type) {
+        this.name = name;
+        this.type = type;
     }
 
     /**
-     * Returns a counter identifier for the given name.
+     * Returns a counter identifier for the given name and type.
      *
      * @param name counter name
+     * @param type counter type
      * @return counter identifier
      */
-    public static PiCounterId of(String name) {
+    public static PiCounterId of(String name, PiCounterType type) {
         checkNotNull(name);
-        checkArgument(!name.isEmpty(), "Name name can't be empty");
-        return new PiCounterId(name);
+        checkNotNull(type);
+        checkArgument(!name.isEmpty(), "Name can't be empty");
+        return new PiCounterId(name, type);
     }
 
     /**
@@ -50,6 +56,38 @@
      * @return counter name
      */
     public String name() {
-        return this.identifier;
+        return this.name;
+    }
+
+    /**
+     * Returns the type of the counter.
+     *
+     * @return counter type
+     */
+    public PiCounterType type() {
+        return this.type;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof PiCounterId)) {
+            return false;
+        }
+        PiCounterId that = (PiCounterId) o;
+        return Objects.equal(name, that.name) &&
+                type == that.type;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(name, type);
+    }
+
+    @Override
+    public String toString() {
+        return type.name().toLowerCase() + ":" + name;
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterType.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterType.java
new file mode 100644
index 0000000..b4a709a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiCounterType.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+/**
+ * Type of counter in a protocol-independent pipeline.
+ */
+public enum PiCounterType {
+    /**
+     * Identifies a counter associated to a match-action table, where cells are directly associated to table entries.
+     */
+    DIRECT,
+
+    /**
+     * Identifies a counter not associated with any other resource.
+     */
+    INDIRECT
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiDirectCounterCellId.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiDirectCounterCellId.java
new file mode 100644
index 0000000..26ae363
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiDirectCounterCellId.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.onosproject.net.pi.runtime.PiCounterType.DIRECT;
+
+/**
+ * Identifier of a direct counter cell of a protocol-independent pipeline.
+ */
+@Beta
+public final class PiDirectCounterCellId implements PiCounterCellId {
+
+    private final PiCounterId counterId;
+    private final PiTableEntry tableEntry;
+
+    private PiDirectCounterCellId(PiCounterId counterId, PiTableEntry tableEntry) {
+        this.counterId = counterId;
+        this.tableEntry = tableEntry;
+    }
+
+    /**
+     * Returns a direct counter cell identifier for the given counter identifier and table entry.
+     *
+     * @param counterId  counter identifier
+     * @param tableEntry table entry
+     * @return direct counter cell identifier
+     */
+    public static PiDirectCounterCellId of(PiCounterId counterId, PiTableEntry tableEntry) {
+        checkNotNull(counterId);
+        checkNotNull(tableEntry);
+        checkArgument(counterId.type() == DIRECT, "Counter ID must be of type DIRECT");
+        return new PiDirectCounterCellId(counterId, tableEntry);
+    }
+
+    /**
+     * Returns the table entry associated with this cell identifier.
+     *
+     * @return cell table entry
+     */
+    public PiTableEntry tableEntry() {
+        return tableEntry;
+    }
+
+    @Override
+    public PiCounterId counterId() {
+        return counterId;
+    }
+
+    @Override
+    public PiCounterType type() {
+        return DIRECT;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof PiDirectCounterCellId)) {
+            return false;
+        }
+        PiDirectCounterCellId that = (PiDirectCounterCellId) o;
+        return Objects.equal(counterId, that.counterId) &&
+                Objects.equal(tableEntry, that.tableEntry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(counterId, tableEntry);
+    }
+
+    @Override
+    public String toString() {
+        return format("%s[{%s}]", counterId, tableEntry);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiIndirectCounterCellId.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiIndirectCounterCellId.java
new file mode 100644
index 0000000..6f3f73a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiIndirectCounterCellId.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import org.onlab.util.Identifier;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.pi.runtime.PiCounterType.INDIRECT;
+
+/**
+ * Identifier of an indirect counter cell in a protocol-independent pipeline.
+ */
+@Beta
+public final class PiIndirectCounterCellId extends Identifier<String> implements PiCounterCellId {
+
+    private final PiCounterId counterId;
+    private final long index;
+
+    private PiIndirectCounterCellId(PiCounterId counterId, long index) {
+        super(counterId.toString() + "[" + index + "]");
+        this.counterId = counterId;
+        this.index = index;
+    }
+
+    /**
+     * Returns a counter cell identifier for the given counter identifier and index.
+     *
+     * @param counterId counter identifier
+     * @param index     index
+     * @return counter cell identifier
+     */
+    public static PiIndirectCounterCellId of(PiCounterId counterId, long index) {
+        checkNotNull(counterId);
+        checkArgument(counterId.type() == INDIRECT, "Counter ID must be of type INDIRECT");
+        checkArgument(index >= 0, "Index must be a positive integer");
+        return new PiIndirectCounterCellId(counterId, index);
+    }
+
+    /**
+     * Returns the index of this cell.
+     *
+     * @return cell index
+     */
+    public long index() {
+        return index;
+    }
+
+    @Override
+    public PiCounterId counterId() {
+        return counterId;
+    }
+
+    @Override
+    public PiCounterType type() {
+        return INDIRECT;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
index 91cabfd..e2e8c3d 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
@@ -24,14 +24,14 @@
 import java.util.Optional;
 import java.util.StringJoiner;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 /**
  * Representation of all field matches of an entry of a match+action table of a protocol-independent pipeline.
  */
 @Beta
 public final class PiMatchKey {
 
+    public static final PiMatchKey EMPTY = builder().build();
+
     private final ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches;
 
     private PiMatchKey(ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches) {
@@ -130,7 +130,6 @@
          */
         public PiMatchKey build() {
             ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches = fieldMatchesBuilder.build();
-            checkArgument(fieldMatches.size() > 0, "Field matches cannot be empty");
             return new PiMatchKey(fieldMatches);
         }
     }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index b0192fd..6a5f75b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -31,6 +31,8 @@
 @Beta
 public final class PiTableEntry {
 
+    public static final PiTableEntry EMTPY = new PiTableEntry();
+
     private static final int NO_PRIORITY = -1;
     private static final double NO_TIMEOUT = -1;
 
@@ -41,6 +43,15 @@
     private final int priority;
     private final double timeout;
 
+    private PiTableEntry() {
+        this.tableId = null;
+        this.matchKey = null;
+        this.tableAction = null;
+        this.cookie = 0;
+        this.priority = NO_PRIORITY;
+        this.timeout = NO_TIMEOUT;
+    }
+
     private PiTableEntry(PiTableId tableId, PiMatchKey matchKey,
                          PiTableAction tableAction, long cookie, int priority, double timeout) {
         this.tableId = tableId;
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
index 505dba3..6caa63c 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
@@ -27,29 +27,27 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
-import static org.onosproject.net.pi.runtime.PiConstantsTest.DROP;
-import static org.onosproject.net.pi.runtime.PiConstantsTest.DST_ADDR;
-import static org.onosproject.net.pi.runtime.PiConstantsTest.IPV4_HEADER_NAME;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.*;
 
 /**
  * Unit tests for PiTableEntry class.
  */
 public class PiTableEntryTest {
-    final PiTableEntry piTableEntry1 = PiTableEntry.builder()
+    private final PiTableEntry piTableEntry1 = PiTableEntry.builder()
             .forTable(PiTableId.of("Table10"))
             .withCookie(0xac)
             .withPriority(10)
             .withAction(PiAction.builder().withId(PiActionId.of(DROP)).build())
             .withTimeout(100)
             .build();
-    final PiTableEntry sameAsPiTableEntry1 = PiTableEntry.builder()
+    private final PiTableEntry sameAsPiTableEntry1 = PiTableEntry.builder()
             .forTable(PiTableId.of("Table10"))
             .withCookie(0xac)
             .withPriority(10)
             .withAction(PiAction.builder().withId(PiActionId.of(DROP)).build())
             .withTimeout(100)
             .build();
-    final PiTableEntry piTableEntry2 = PiTableEntry.builder()
+    private final PiTableEntry piTableEntry2 = PiTableEntry.builder()
             .forTable(PiTableId.of("Table20"))
             .withCookie(0xac)
             .withPriority(10)
@@ -77,6 +75,16 @@
     }
 
     /**
+     * Tests equality of the empty table entry.
+     */
+    @Test
+    public void testEmptyEquals() {
+        new EqualsTester()
+                .addEqualityGroup(PiTableEntry.EMTPY, PiTableEntry.EMTPY)
+                .testEquals();
+    }
+
+    /**
      * Tests creation of a DefaultFlowRule using a FlowRule constructor.
      */
     @Test
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java b/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java
index 794e7f8..a9178ca 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java
@@ -32,6 +32,7 @@
 import org.onosproject.net.pi.runtime.PiActionId;
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiActionParamId;
+import org.onosproject.net.pi.runtime.PiCounterId;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiTableId;
@@ -103,6 +104,11 @@
     }
 
     @Override
+    public Optional<PiCounterId> mapTableCounter(PiTableId piTableId) {
+        return Optional.empty();
+    }
+
+    @Override
     public Collection<PiPacketOperation> mapOutboundPacket(OutboundPacket packet)
             throws PiInterpreterException {
         return ImmutableList.of();
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4Interpreter.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4Interpreter.java
index 6029590..da56bfc 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4Interpreter.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4Interpreter.java
@@ -43,6 +43,8 @@
 import org.onosproject.net.pi.runtime.PiActionId;
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiActionParamId;
+import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiCounterType;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiPacketMetadata;
 import org.onosproject.net.pi.runtime.PiPacketMetadataId;
@@ -74,6 +76,7 @@
     // e.g. in a dedicated onos/pipeconf directory, along with any related P4 source code.
 
     public static final String TABLE0 = "table0";
+    public static final String TABLE0_COUNTER = "table0_counter";
     public static final String SEND_TO_CPU = "send_to_cpu";
     public static final String PORT = "port";
     public static final String DROP = "_drop";
@@ -81,12 +84,17 @@
     public static final String EGRESS_PORT = "egress_port";
     public static final String INGRESS_PORT = "ingress_port";
 
+    private static final PiTableId TABLE0_ID = PiTableId.of(TABLE0);
+
     protected static final PiHeaderFieldId ETH_DST_ID = PiHeaderFieldId.of("ethernet", "dstAddr");
     protected static final PiHeaderFieldId ETH_SRC_ID = PiHeaderFieldId.of("ethernet", "srcAddr");
     protected static final PiHeaderFieldId ETH_TYPE_ID = PiHeaderFieldId.of("ethernet", "etherType");
 
     private static final ImmutableBiMap<Integer, PiTableId> TABLE_MAP = ImmutableBiMap.of(
-            0, PiTableId.of(TABLE0));
+            0, TABLE0_ID);
+
+    private static final ImmutableBiMap<PiTableId, PiCounterId> TABLE_COUNTER_MAP = ImmutableBiMap.of(
+            TABLE0_ID, PiCounterId.of(TABLE0_COUNTER, PiCounterType.DIRECT));
 
     private boolean targetAttributesInitialized = false;
 
@@ -189,6 +197,11 @@
     }
 
     @Override
+    public Optional<PiCounterId> mapTableCounter(PiTableId piTableId) {
+        return Optional.ofNullable(TABLE_COUNTER_MAP.get(piTableId));
+    }
+
+    @Override
     public Collection<PiPacketOperation> mapOutboundPacket(OutboundPacket packet)
             throws PiInterpreterException {
         TrafficTreatment treatment = packet.treatment();
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4PortStatisticsDiscovery.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4PortStatisticsDiscovery.java
index 2946304..b127855 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4PortStatisticsDiscovery.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/DefaultP4PortStatisticsDiscovery.java
@@ -24,6 +24,7 @@
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiIndirectCounterCellId;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -32,6 +33,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
+import static org.onosproject.net.pi.runtime.PiCounterType.INDIRECT;
+
 /**
  * Implementation of a PortStatisticsBehaviour that can be used for any P4 program based on default.p4 (i.e. those
  * under onos/tools/test/p4src).
@@ -39,8 +42,8 @@
 public class DefaultP4PortStatisticsDiscovery extends AbstractP4RuntimeHandlerBehaviour
         implements PortStatisticsDiscovery {
 
-    private static final PiCounterId INGRESS_COUNTER_ID = PiCounterId.of("ingress_port_counter");
-    private static final PiCounterId EGRESS_COUNTER_ID = PiCounterId.of("egress_port_counter");
+    private static final PiCounterId INGRESS_COUNTER_ID = PiCounterId.of("ingress_port_counter", INDIRECT);
+    private static final PiCounterId EGRESS_COUNTER_ID = PiCounterId.of("egress_port_counter", INDIRECT);
 
     @Override
     public Collection<PortStatistics> discoverPortStatistics() {
@@ -54,14 +57,14 @@
         deviceService.getPorts(deviceId)
                 .forEach(p -> portStatBuilders.put(p.number().toLong(),
                                                    DefaultPortStatistics.builder()
-                                                           .setPort((int) p.number().toLong())
+                                                           .setPort(p.number())
                                                            .setDeviceId(deviceId)));
 
         Set<PiCounterCellId> counterCellIds = Sets.newHashSet();
         portStatBuilders.keySet().forEach(p -> {
             // Counter cell/index = port number.
-            counterCellIds.add(PiCounterCellId.of(INGRESS_COUNTER_ID, p));
-            counterCellIds.add(PiCounterCellId.of(EGRESS_COUNTER_ID, p));
+            counterCellIds.add(PiIndirectCounterCellId.of(INGRESS_COUNTER_ID, p));
+            counterCellIds.add(PiIndirectCounterCellId.of(EGRESS_COUNTER_ID, p));
         });
 
         Collection<PiCounterCellData> counterEntryResponse;
@@ -73,20 +76,25 @@
             return Collections.emptyList();
         }
 
-        counterEntryResponse.forEach(counterEntry -> {
-            if (!portStatBuilders.containsKey(counterEntry.cellId().index())) {
-                log.warn("Unrecognized counter index {}, skipping", counterEntry);
+        counterEntryResponse.forEach(counterData -> {
+            if (counterData.cellId().type() != INDIRECT) {
+                log.warn("Invalid counter data type {}, skipping", counterData.cellId().type());
                 return;
             }
-            DefaultPortStatistics.Builder statsBuilder = portStatBuilders.get(counterEntry.cellId().index());
-            if (counterEntry.cellId().counterId().equals(INGRESS_COUNTER_ID)) {
-                statsBuilder.setPacketsReceived(counterEntry.packets());
-                statsBuilder.setBytesReceived(counterEntry.bytes());
-            } else if (counterEntry.cellId().counterId().equals(EGRESS_COUNTER_ID)) {
-                statsBuilder.setPacketsSent(counterEntry.packets());
-                statsBuilder.setBytesSent(counterEntry.bytes());
+            PiIndirectCounterCellId indCellId = (PiIndirectCounterCellId) counterData.cellId();
+            if (!portStatBuilders.containsKey(indCellId.index())) {
+                log.warn("Unrecognized counter index {}, skipping", counterData);
+                return;
+            }
+            DefaultPortStatistics.Builder statsBuilder = portStatBuilders.get(indCellId.index());
+            if (counterData.cellId().counterId().equals(INGRESS_COUNTER_ID)) {
+                statsBuilder.setPacketsReceived(counterData.packets());
+                statsBuilder.setBytesReceived(counterData.bytes());
+            } else if (counterData.cellId().counterId().equals(EGRESS_COUNTER_ID)) {
+                statsBuilder.setPacketsSent(counterData.packets());
+                statsBuilder.setBytesSent(counterData.bytes());
             } else {
-                log.warn("Unrecognized counter ID {}, skipping", counterEntry);
+                log.warn("Unrecognized counter ID {}, skipping", counterData);
             }
         });
 
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
index ad9329a..50b808e 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import io.grpc.StatusRuntimeException;
 import org.onosproject.net.flow.DefaultFlowEntry;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
@@ -26,6 +27,10 @@
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.model.PiPipelineModel;
 import org.onosproject.net.pi.model.PiTableModel;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiDirectCounterCellId;
 import org.onosproject.net.pi.runtime.PiFlowRuleTranslationService;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableId;
@@ -36,6 +41,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.locks.Lock;
@@ -72,6 +79,13 @@
     // TODO: can remove this check as soon as the BMv2 bug when reading ECMP entries is fixed.
     private boolean ignoreDeviceWhenGet = true;
 
+    /*
+    If true, we read all direct counters of a table with one request. Otherwise, send as many request as the number of
+    table entries.
+     */
+    // TODO: set to true as soon as the feature is implemented in P4Runtime.
+    private boolean readAllDirectCounters = false;
+
     // Needed to synchronize operations over the same table entry.
     private static final ConcurrentMap<P4RuntimeTableEntryReference, Lock> ENTRY_LOCKS = Maps.newConcurrentMap();
 
@@ -132,33 +146,70 @@
 
             Collection<PiTableEntry> installedEntries;
             try {
+                // TODO: optimize by dumping entries and counters in parallel, from ALL tables with the same request.
                 installedEntries = client.dumpTable(piTableId, pipeconf).get();
             } catch (InterruptedException | ExecutionException e) {
-                log.error("Exception while dumping table {} of {}", piTableId, deviceId, e);
+                if (!(e.getCause() instanceof StatusRuntimeException)) {
+                    // gRPC errors are logged in the client.
+                    log.error("Exception while dumping table {} of {}", piTableId, deviceId, e);
+                }
                 return Collections.emptyList();
             }
 
+            Map<PiTableEntry, PiCounterCellData> counterCellMap;
+            try {
+                if (interpreter.mapTableCounter(piTableId).isPresent()) {
+                    PiCounterId piCounterId = interpreter.mapTableCounter(piTableId).get();
+                    Collection<PiCounterCellData> cellDatas;
+                    if (readAllDirectCounters) {
+                        cellDatas = client.readAllCounterCells(Collections.singleton(piCounterId), pipeconf).get();
+                    } else {
+                        Set<PiCounterCellId> cellIds = installedEntries.stream()
+                                .map(entry -> PiDirectCounterCellId.of(piCounterId, entry))
+                                .collect(Collectors.toSet());
+                        cellDatas = client.readCounterCells(cellIds, pipeconf).get();
+                    }
+                    counterCellMap = cellDatas.stream()
+                            .collect(Collectors.toMap(c -> ((PiDirectCounterCellId) c.cellId()).tableEntry(), c -> c));
+                } else {
+                    counterCellMap = Collections.emptyMap();
+                }
+                installedEntries = client.dumpTable(piTableId, pipeconf).get();
+            } catch (InterruptedException | ExecutionException e) {
+                if (!(e.getCause() instanceof StatusRuntimeException)) {
+                    // gRPC errors are logged in the client.
+                    log.error("Exception while reading counters of table {} of {}", piTableId, deviceId, e);
+                }
+                counterCellMap = Collections.emptyMap();
+            }
+
             for (PiTableEntry installedEntry : installedEntries) {
 
-                P4RuntimeTableEntryReference entryRef = new P4RuntimeTableEntryReference(deviceId, piTableId,
+                P4RuntimeTableEntryReference entryRef = new P4RuntimeTableEntryReference(deviceId,
+                                                                                         piTableId,
                                                                                          installedEntry.matchKey());
 
-                P4RuntimeFlowRuleWrapper frWrapper = ENTRY_STORE.get(entryRef);
-
-
-                if (frWrapper == null) {
+                if (!ENTRY_STORE.containsKey(entryRef)) {
                     // Inconsistent entry
                     inconsistentEntries.add(installedEntry);
                     continue; // next one.
                 }
 
-                // TODO: implement table entry counter retrieval.
+                P4RuntimeFlowRuleWrapper frWrapper = ENTRY_STORE.get(entryRef);
+
                 long bytes = 0L;
                 long packets = 0L;
+                if (counterCellMap.containsKey(installedEntry)) {
+                    PiCounterCellData counterCellData = counterCellMap.get(installedEntry);
+                    bytes = counterCellData.bytes();
+                    packets = counterCellData.packets();
+                }
 
-                FlowEntry entry = new DefaultFlowEntry(frWrapper.rule(), ADDED, frWrapper.lifeInSeconds(),
-                                                       packets, bytes);
-                resultBuilder.add(entry);
+                resultBuilder.add(new DefaultFlowEntry(frWrapper.rule(),
+                                                       ADDED,
+                                                       frWrapper.lifeInSeconds(),
+                                                       packets,
+                                                       bytes));
             }
         }
 
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
new file mode 100644
index 0000000..025a5a3
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
@@ -0,0 +1,201 @@
+/*
+ * 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.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiDirectCounterCellId;
+import org.onosproject.net.pi.runtime.PiIndirectCounterCellId;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.slf4j.Logger;
+import p4.P4RuntimeOuterClass.CounterData;
+import p4.P4RuntimeOuterClass.CounterEntry;
+import p4.P4RuntimeOuterClass.DirectCounterEntry;
+import p4.P4RuntimeOuterClass.Entity;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+import static p4.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
+import static p4.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.
+     * <p>
+     * This method takes as parameter also a map between numeric P4Info IDs and PI counter IDs, that will be populated
+     * during the process and that is then needed to aid in the decode process.
+     *
+     * @param cellIds      counter cell identifiers
+     * @param counterIdMap counter ID map (empty, it will be populated during this method execution)
+     * @param pipeconf     pipeconf
+     * @return collection of entity messages describing both counter or direct counter entries
+     */
+    static Collection<Entity> encodePiCounterCellIds(Collection<PiCounterCellId> cellIds,
+                                                     Map<Integer, PiCounterId> counterIdMap,
+                                                     PiPipeconf pipeconf) {
+        return cellIds
+                .stream()
+                .map(cellId -> {
+                    try {
+                        return encodePiCounterCellId(cellId, counterIdMap, pipeconf);
+                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                        log.warn("Unable to encode PI counter cell id: {}", 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, for the given counter ID map (populated by {@link
+     * #encodePiCounterCellIds(Collection, Map, PiPipeconf)}), 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 counterIdMap counter ID map (previously populated)
+     * @param pipeconf     pipeconf
+     * @return collection of PI counter cell data
+     */
+    static Collection<PiCounterCellData> decodeCounterEntities(Collection<Entity> entities,
+                                                               Map<Integer, PiCounterId> counterIdMap,
+                                                               PiPipeconf pipeconf) {
+        return entities
+                .stream()
+                .filter(entity -> entity.getEntityCase() == COUNTER_ENTRY ||
+                        entity.getEntityCase() == DIRECT_COUNTER_ENTRY)
+                .map(entity -> {
+                    try {
+                        return decodeCounterEntity(entity, counterIdMap, pipeconf);
+                    } catch (EncodeException | 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, Map<Integer, PiCounterId> counterIdMap,
+                                                PiPipeconf pipeconf)
+            throws P4InfoBrowser.NotFoundException, EncodeException {
+
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+
+        int counterId;
+        Entity entity;
+        // Encode PI cell ID into entity message and add to read request.
+        switch (cellId.type()) {
+            case INDIRECT:
+                counterId = browser.counters().getByNameOrAlias(cellId.counterId().name()).getPreamble().getId();
+                PiIndirectCounterCellId indCellId = (PiIndirectCounterCellId) cellId;
+                entity = Entity.newBuilder().setCounterEntry(CounterEntry.newBuilder()
+                                                                     .setCounterId(counterId)
+                                                                     .setIndex(indCellId.index())
+                                                                     .build())
+                        .build();
+                break;
+            case DIRECT:
+                counterId = browser.directCounters().getByNameOrAlias(cellId.counterId().name()).getPreamble().getId();
+                PiDirectCounterCellId dirCellId = (PiDirectCounterCellId) cellId;
+                DirectCounterEntry.Builder entryBuilder = DirectCounterEntry.newBuilder().setCounterId(counterId);
+                if (!dirCellId.tableEntry().equals(PiTableEntry.EMTPY)) {
+                    entryBuilder.setTableEntry(TableEntryEncoder.encode(dirCellId.tableEntry(), pipeconf));
+                }
+                entity = Entity.newBuilder().setDirectCounterEntry(entryBuilder.build()).build();
+                break;
+            default:
+                throw new EncodeException(format("Unrecognized PI counter cell ID type '%s'", cellId.type()));
+        }
+        counterIdMap.put(counterId, cellId.counterId());
+
+        return entity;
+    }
+
+    private static PiCounterCellData decodeCounterEntity(Entity entity, Map<Integer, PiCounterId> counterIdMap,
+                                                         PiPipeconf pipeconf)
+            throws EncodeException, P4InfoBrowser.NotFoundException {
+
+        int counterId;
+        CounterData counterData;
+
+        if (entity.getEntityCase() == COUNTER_ENTRY) {
+            counterId = entity.getCounterEntry().getCounterId();
+            counterData = entity.getCounterEntry().getData();
+        } else {
+            counterId = entity.getDirectCounterEntry().getCounterId();
+            counterData = entity.getDirectCounterEntry().getData();
+        }
+
+        // Process only counter IDs that were requested in the first place.
+        if (!counterIdMap.containsKey(counterId)) {
+            throw new EncodeException(format("Unrecognized counter ID '%s'", counterId));
+        }
+
+        PiCounterId piCounterId = counterIdMap.get(counterId);
+
+        // Compute PI cell ID.
+        PiCounterCellId piCellId;
+
+        switch (piCounterId.type()) {
+            case INDIRECT:
+                if (entity.getEntityCase() != COUNTER_ENTRY) {
+                    throw new EncodeException(format(
+                            "Counter ID '%s' is indirect, but processed entity is %s",
+                            piCounterId, entity.getEntityCase()));
+                }
+                piCellId = PiIndirectCounterCellId.of(piCounterId,
+                                                      entity.getCounterEntry().getIndex());
+                break;
+            case DIRECT:
+                if (entity.getEntityCase() != DIRECT_COUNTER_ENTRY) {
+                    throw new EncodeException(format(
+                            "Counter ID '%s' is direct, but processed entity is %s",
+                            piCounterId, entity.getEntityCase()));
+                }
+                PiTableEntry piTableEntry = TableEntryEncoder.decode(entity.getDirectCounterEntry().getTableEntry(),
+                                                                     pipeconf);
+                piCellId = PiDirectCounterCellId.of(piCounterId, piTableEntry);
+                break;
+            default:
+                throw new EncodeException(format("Unrecognized PI counter ID type '%s'", piCounterId.type()));
+        }
+
+        return new PiCounterCellData(piCellId, counterData.getPacketCount(), counterData.getByteCount());
+    }
+}
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
index 00a2683..a9adf55 100644
--- 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
@@ -16,9 +16,9 @@
 
 package org.onosproject.p4runtime.ctl;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import com.google.protobuf.ByteString;
 import io.grpc.Context;
 import io.grpc.ManagedChannel;
@@ -31,6 +31,8 @@
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiDirectCounterCellId;
+import org.onosproject.net.pi.runtime.PiIndirectCounterCellId;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiPipeconfService;
 import org.onosproject.net.pi.runtime.PiTableEntry;
@@ -39,7 +41,6 @@
 import org.onosproject.p4runtime.api.P4RuntimeEvent;
 import org.slf4j.Logger;
 import p4.P4RuntimeGrpc;
-import p4.P4RuntimeOuterClass.CounterEntry;
 import p4.P4RuntimeOuterClass.Entity;
 import p4.P4RuntimeOuterClass.ForwardingPipelineConfig;
 import p4.P4RuntimeOuterClass.MasterArbitrationUpdate;
@@ -77,7 +78,6 @@
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType;
 import static org.slf4j.LoggerFactory.getLogger;
-import static p4.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
 import static p4.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
 import static p4.P4RuntimeOuterClass.PacketOut;
 import static p4.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest.Action.VERIFY_AND_COMMIT;
@@ -87,11 +87,6 @@
  */
 public final class P4RuntimeClientImpl implements P4RuntimeClient {
 
-    private static final int DEADLINE_SECONDS = 15;
-
-    // FIXME: use static election ID, since mastership arbitration is not yet support on BMv2 or Tofino.
-    private static final int ELECTION_ID = 1;
-
     private static final Map<WriteOperationType, Update.Type> UPDATE_TYPES = ImmutableMap.of(
             WriteOperationType.UNSPECIFIED, Update.Type.UNSPECIFIED,
             WriteOperationType.INSERT, Update.Type.INSERT,
@@ -142,7 +137,11 @@
             try {
                 return supplier.get();
             } catch (Throwable ex) {
-                log.error("Exception in P4Runtime client of {}, executing {}", deviceId, opDescription, ex);
+                if (ex instanceof StatusRuntimeException) {
+                    log.warn("Unable to execute {} on {}: {}", opDescription, deviceId, ex.toString());
+                } else {
+                    log.error("Exception in client of {}, executing {}", deviceId, opDescription, ex);
+                }
                 throw ex;
             } finally {
                 writeLock.unlock();
@@ -187,10 +186,31 @@
     @Override
     public CompletableFuture<Collection<PiCounterCellData>> readAllCounterCells(Set<PiCounterId> counterIds,
                                                                                 PiPipeconf pipeconf) {
-        Set<PiCounterCellId> cellIds = counterIds.stream()
-                // Cell with index 0 means all cells.
-                .map(counterId -> PiCounterCellId.of(counterId, 0))
-                .collect(Collectors.toSet());
+
+        /*
+        From p4runtime.proto, the scope of a ReadRequest is defined as follows:
+        CounterEntry:
+            - All counter cells for all meters if counter_id = 0 (default).
+            - All counter cells for given counter_id if index = 0 (default).
+        DirectCounterEntry:
+            - All counter cells for all meters if counter_id = 0 (default).
+            - All counter cells for given counter_id if table_entry.match is empty.
+         */
+
+        Set<PiCounterCellId> cellIds = Sets.newHashSet();
+
+        for (PiCounterId counterId : counterIds) {
+            switch (counterId.type()) {
+                case INDIRECT:
+                    cellIds.add(PiIndirectCounterCellId.of(counterId, 0));
+                    break;
+                case DIRECT:
+                    cellIds.add(PiDirectCounterCellId.of(counterId, PiTableEntry.EMTPY));
+                    break;
+                default:
+                    log.warn("Unrecognized PI counter ID '{}'", counterId.type());
+            }
+        }
 
         return supplyInContext(() -> doReadCounterCells(cellIds, pipeconf),
                                "readAllCounterCells-" + cellIds.hashCode());
@@ -416,62 +436,32 @@
 
     private Collection<PiCounterCellData> doReadCounterCells(Collection<PiCounterCellId> cellIds, PiPipeconf pipeconf) {
 
-        // From p4runtime.proto:
-        // For ReadRequest, the scope is defined as follows:
-        // - All counter cells for all meters if counter_id = 0 (default).
-        // - All counter cells for given counter_id if index = 0 (default).
-
-        final ReadRequest.Builder requestBuilder = ReadRequest.newBuilder().setDeviceId(p4DeviceId);
-        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        // We use this map to remember the original PI counter IDs of the returned response.
         final Map<Integer, PiCounterId> counterIdMap = Maps.newHashMap();
 
-        for (PiCounterCellId cellId : cellIds) {
-            int counterId;
-            try {
-                counterId = browser.counters().getByNameOrAlias(cellId.counterId().id()).getPreamble().getId();
-            } catch (P4InfoBrowser.NotFoundException e) {
-                log.warn("Skipping counter cell {}: {}", cellId, e.getMessage());
-                continue;
-            }
-            requestBuilder
-                    .addEntities(Entity.newBuilder()
-                                         .setCounterEntry(CounterEntry.newBuilder()
-                                                                  .setCounterId(counterId)
-                                                                  .setIndex(cellId.index())
-                                                                  .build()));
-            counterIdMap.put(counterId, cellId.counterId());
+        final ReadRequest request = ReadRequest.newBuilder()
+                .setDeviceId(p4DeviceId)
+                .addAllEntities(CounterEntryCodec.encodePiCounterCellIds(cellIds, counterIdMap, pipeconf))
+                .build();
+
+        if (request.getEntitiesList().size() == 0) {
+            return Collections.emptyList();
         }
 
-        final Iterator<ReadResponse> responses;
+        final Iterable<ReadResponse> responses;
         try {
-            responses = blockingStub.read(requestBuilder.build());
+            responses = () -> blockingStub.read(request);
         } catch (StatusRuntimeException e) {
             log.warn("Unable to read counters: {}", e.getMessage());
             return Collections.emptyList();
         }
 
-        final Iterable<ReadResponse> responseIterable = () -> responses;
-        final ImmutableList.Builder<PiCounterCellData> piCounterEntryListBuilder = ImmutableList.builder();
-
-        StreamSupport
-                .stream(responseIterable.spliterator(), false)
+        List<Entity> entities = StreamSupport.stream(responses.spliterator(), false)
                 .map(ReadResponse::getEntitiesList)
                 .flatMap(List::stream)
-                .filter(entity -> entity.getEntityCase() == COUNTER_ENTRY)
-                .map(Entity::getCounterEntry)
-                .forEach(counterEntryMsg -> {
-                    if (!counterIdMap.containsKey(counterEntryMsg.getCounterId())) {
-                        log.warn("Unrecognized counter ID '{}', skipping", counterEntryMsg.getCounterId());
-                        return;
-                    }
-                    PiCounterCellId cellId = PiCounterCellId.of(counterIdMap.get(counterEntryMsg.getCounterId()),
-                                                                counterEntryMsg.getIndex());
-                    piCounterEntryListBuilder.add(new PiCounterCellData(cellId,
-                                                                        counterEntryMsg.getData().getPacketCount(),
-                                                                        counterEntryMsg.getData().getByteCount()));
-                });
+                .collect(Collectors.toList());
 
-        return piCounterEntryListBuilder.build();
+        return CounterEntryCodec.decodeCounterEntities(entities, counterIdMap, pipeconf);
     }
 
     /**
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
index 0dd82f3..9ffc18f 100644
--- 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
@@ -70,8 +70,8 @@
     }
 
     /**
-     * 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, it is skipped, hence the returned
+     * 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, 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.
@@ -103,6 +103,26 @@
     }
 
     /**
+     * Same as {@link #encode(Collection, PiPipeconf)} but encodes only one entry.
+     *
+     * @param piTableEntry table entry
+     * @param pipeconf     pipeconf
+     * @return encoded table entry message
+     * @throws EncodeException                 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 EncodeException, P4InfoBrowser.NotFoundException {
+
+        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new EncodeException(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.
@@ -135,12 +155,63 @@
         return piTableEntryListBuilder.build();
     }
 
+    /**
+     * Same as {@link #decode(Collection, PiPipeconf)} but decodes only one entry.
+     *
+     * @param tableEntryMsg table entry message
+     * @param pipeconf      pipeconf
+     * @return decoded PI table entry
+     * @throws EncodeException                 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 EncodeException, P4InfoBrowser.NotFoundException {
+
+        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new EncodeException(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 EncodeException                 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 EncodeException, P4InfoBrowser.NotFoundException {
+
+        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
+
+        //FIXME this throws some kind of NPE
+        P4InfoOuterClass.Table tableInfo = browser.tables().getByName(tableId.id());
+
+        // Table id.
+        tableEntryMsgBuilder.setTableId(tableInfo.getPreamble().getId());
+
+        // Field matches.
+        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, EncodeException {
 
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
 
-        //FIXME this thorws some kind of NPE
+        //FIXME this throws some kind of NPE
         P4InfoOuterClass.Table tableInfo = browser.tables().getByName(piTableEntry.table().id());
 
         // Table id.
@@ -193,11 +264,7 @@
         // FIXME: how to decode table entry messages with timeout, given that the timeout value is lost after encoding?
 
         // Match key for field matches.
-        PiMatchKey.Builder piMatchKeyBuilder = PiMatchKey.builder();
-        for (FieldMatch fieldMatchMsg : tableEntryMsg.getMatchList()) {
-            piMatchKeyBuilder.addFieldMatch(decodeFieldMatchMsg(fieldMatchMsg, tableInfo, browser));
-        }
-        piTableEntryBuilder.withMatchKey(piMatchKeyBuilder.build());
+        piTableEntryBuilder.withMatchKey(decodeFieldMatchMsgs(tableEntryMsg.getMatchList(), tableInfo, browser));
 
         return piTableEntryBuilder.build();
     }
@@ -280,6 +347,33 @@
         }
     }
 
+    /**
+     * 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 EncodeException                 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, EncodeException {
+        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        P4InfoOuterClass.Table tableInfo = browser.tables().getById(tableEntryMsg.getTableId());
+        return decodeFieldMatchMsgs(tableEntryMsg.getMatchList(), tableInfo, browser);
+    }
+
+    private static PiMatchKey decodeFieldMatchMsgs(Collection<FieldMatch> fieldMatchs, P4InfoOuterClass.Table tableInfo,
+                                                   P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, EncodeException {
+        // 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, EncodeException {