TL1 device provider with driver for Lumentum WaveReady.

ONOS-5800 & ONOS-5801

Change-Id: Icd820285eb8db2fd92c03ebf11ce022b6a82b48a
diff --git a/drivers/lumentum/BUCK b/drivers/lumentum/BUCK
index 0feb6ee..02ea7ea 100644
--- a/drivers/lumentum/BUCK
+++ b/drivers/lumentum/BUCK
@@ -3,6 +3,8 @@
     '//lib:org.apache.servicemix.bundles.snmp4j',
     '//drivers/utilities:onos-drivers-utilities',
     '//protocols/snmp/api:onos-protocols-snmp-api',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+    '//protocols/tl1/ctl:onos-protocols-tl1-ctl',
     '//incubator/api:onos-incubator-api',
     '//apps/optical-model:onos-apps-optical-model',
 ]
@@ -25,5 +27,6 @@
     category = 'Drivers',
     url = 'http://onosproject.org',
     description = 'ONOS Lumentum Device Drivers application.',
-    required_apps = [ 'org.onosproject.snmp', 'org.onosproject.faultmanagement', 'org.onosproject.optical-model' ],
+    required_apps = [ 'org.onosproject.snmp', 'org.onosproject.faultmanagement', 'org.onosproject.optical-model',
+     'org.onosproject.tl1'],
 )
diff --git a/drivers/lumentum/pom.xml b/drivers/lumentum/pom.xml
index fdfd5a3..6dce6cd 100644
--- a/drivers/lumentum/pom.xml
+++ b/drivers/lumentum/pom.xml
@@ -38,7 +38,8 @@
         <onos.app.requires>
             org.onosproject.snmp,
             org.onosproject.faultmanagement,
-            org.onosproject.optical-model
+            org.onosproject.optical-model,
+            org.onosproject.tl1
         </onos.app.requires>
     </properties>
 
@@ -59,5 +60,15 @@
             <artifactId>onos-snmp-api</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-ctl</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
 </project>
diff --git a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDeviceDescription.java b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDiscovery.java
similarity index 97%
rename from drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDeviceDescription.java
rename to drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDiscovery.java
index 46e0887..c49d6ed 100644
--- a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDeviceDescription.java
+++ b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumRoadmDiscovery.java
@@ -46,7 +46,7 @@
 /**
  * Device description behaviour for Lumentum Snmp devices.
  */
-public class LumentumRoadmDeviceDescription extends AbstractHandlerBehaviour implements DeviceDescriptionDiscovery {
+public class LumentumRoadmDiscovery extends AbstractHandlerBehaviour implements DeviceDescriptionDiscovery {
 
     private final Logger log = getLogger(getClass());
 
diff --git a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumFlowRuleProgrammable.java b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmFlowRuleProgrammable.java
similarity index 98%
rename from drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumFlowRuleProgrammable.java
rename to drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmFlowRuleProgrammable.java
index 21f9b24..7dde491 100644
--- a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumFlowRuleProgrammable.java
+++ b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmFlowRuleProgrammable.java
@@ -58,10 +58,10 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 // TODO: need to convert between OChSignal and XC channel number
-public class LumentumFlowRuleProgrammable extends AbstractHandlerBehaviour implements FlowRuleProgrammable {
+public class LumentumSdnRoadmFlowRuleProgrammable extends AbstractHandlerBehaviour implements FlowRuleProgrammable {
 
     private static final Logger log =
-            LoggerFactory.getLogger(LumentumFlowRuleProgrammable.class);
+            LoggerFactory.getLogger(LumentumSdnRoadmFlowRuleProgrammable.class);
 
     // Default values
     private static final int DEFAULT_TARGET_GAIN_PREAMP = 150;
diff --git a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LambdaQueryLumentumRoadm.java b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmLambdaQuery.java
similarity index 94%
rename from drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LambdaQueryLumentumRoadm.java
rename to drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmLambdaQuery.java
index e61dc24..55f2aa7 100644
--- a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LambdaQueryLumentumRoadm.java
+++ b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumSdnRoadmLambdaQuery.java
@@ -31,7 +31,7 @@
  *
  * Device supports 96 wavelengths of 50 GHz, between center frequencies 191.350 THz and 196.075 THz.
  */
-public class LambdaQueryLumentumRoadm extends AbstractHandlerBehaviour implements LambdaQuery {
+public class LumentumSdnRoadmLambdaQuery extends AbstractHandlerBehaviour implements LambdaQuery {
     private static final int LAMBDA_COUNT = 96;
 
     @Override
diff --git a/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumWaveReadyDiscovery.java b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumWaveReadyDiscovery.java
new file mode 100644
index 0000000..cf9ad21
--- /dev/null
+++ b/drivers/lumentum/src/main/java/org/onosproject/drivers/lumentum/LumentumWaveReadyDiscovery.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.drivers.lumentum;
+
+import org.onlab.packet.ChassisId;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.CltSignalType;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.GridType;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.OduSignalType;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.SparseAnnotations;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceDescriptionDiscovery;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.tl1.Tl1Command;
+import org.onosproject.tl1.Tl1Controller;
+import org.onosproject.tl1.Tl1Device;
+import org.onosproject.tl1.impl.DefaultTl1Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.optical.device.OchPortHelper.ochPortDescription;
+import static org.onosproject.net.optical.device.OduCltPortHelper.oduCltPortDescription;
+
+/**
+ * Device description behaviour for Lumentum WaveReady devices.
+ *
+ * Tested with Lumentum WaveReady 3100 transponder.
+ */
+public class LumentumWaveReadyDiscovery extends AbstractHandlerBehaviour implements DeviceDescriptionDiscovery {
+    private final Logger log = LoggerFactory.getLogger(LumentumWaveReadyDiscovery.class);
+
+    // Time to wait for device response in milliseconds
+    private static final int TIMEOUT = 10000;
+
+    private static final String LUMENTUM = "Lumentum";
+    private static final String WAVEREADY = "WaveReady";
+    private static final String SWVERSION = "1.0";
+    private static final String SERIAL = "3100";
+
+    // Some TL1 string constants
+    private static final String ACT = "ACT";
+    private static final String USER = "USER";
+    private static final String RTRV = "RTRV";
+    private static final String NETYPE = "NETYPE";
+    private static final String PLUGGABLE_INV = "PLUGGABLE-INV";
+    private static final String CANC = "CANC";
+    private static final String EIGHTFIFTY = "850";
+
+
+    @Override
+    public DeviceDescription discoverDeviceDetails() {
+        DeviceId deviceId = handler().data().deviceId();
+        Tl1Controller ctrl = checkNotNull(handler().get(Tl1Controller.class));
+        // Something reasonable, unavailable by default
+        DeviceDescription defaultDescription = new DefaultDeviceDescription(deviceId.uri(), Device.Type.OTN,
+                LUMENTUM, WAVEREADY, SWVERSION, SERIAL,
+                new ChassisId(), false, DefaultAnnotations.EMPTY);
+
+        Optional<Tl1Device> device = ctrl.getDevice(deviceId);
+        if (!device.isPresent()) {
+            return defaultDescription;
+        }
+
+        // Login
+        Tl1Command loginCmd = DefaultTl1Command.builder()
+                .withVerb(ACT)
+                .withModifier(USER)
+                .withAid(device.get().username())
+                .withCtag(100)
+                .withParameters(device.get().password())
+                .build();
+        Future<String> login = ctrl.sendMsg(deviceId, loginCmd);
+
+        try {
+            String loginResponse = login.get(TIMEOUT, TimeUnit.MILLISECONDS);
+            if (loginResponse.contains("Access denied")) {
+                log.error("Access denied: {}", loginResponse);
+                return defaultDescription;
+            }
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("Login failed", e);
+            return defaultDescription;
+        }
+
+        // Fetch device description
+        Tl1Command ddCmd = DefaultTl1Command.builder()
+                .withVerb(RTRV)
+                .withModifier(NETYPE)
+                .withCtag(101)
+                .build();
+        Future<String> dd = ctrl.sendMsg(deviceId, ddCmd);
+
+        try {
+            String ddResponse = dd.get(TIMEOUT, TimeUnit.MILLISECONDS);
+
+            return new DefaultDeviceDescription(defaultDescription, true, extractAnnotations(ddResponse));
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("Device description not found", e);
+            return defaultDescription;
+        }
+    }
+
+    @Override
+    public List<PortDescription> discoverPortDetails() {
+        DeviceId deviceId = handler().data().deviceId();
+        Tl1Controller ctrl = checkNotNull(handler().get(Tl1Controller.class));
+
+        // Assume we're successfully logged in
+        // Fetch port descriptions
+        Tl1Command pdCmd = DefaultTl1Command.builder()
+                .withVerb(RTRV)
+                .withModifier(PLUGGABLE_INV)
+                .withCtag(102)
+                .build();
+        Future<String> pd = ctrl.sendMsg(deviceId, pdCmd);
+
+        try {
+            String pdResponse = pd.get(TIMEOUT, TimeUnit.MILLISECONDS);
+
+            return extractPorts(pdResponse);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("Port description not found", e);
+            return Collections.EMPTY_LIST;
+        }
+    }
+
+    private SparseAnnotations extractAnnotations(String s) {
+        DefaultAnnotations.Builder annot = DefaultAnnotations.builder();
+
+        Arrays.stream(s.split(",")).forEach(w -> {
+            String[] pair = w.replaceAll("\\\\\"", "").split("=");
+            if (pair.length == 2) {
+                annot.set(pair[0], pair[1]);
+            } else {
+                annot.set(pair[0], "");
+            }
+        });
+
+        return annot.build();
+    }
+
+    // Extract ports from response on pluggable inventory retrieval.
+    // Client ports are identified by 850nm, everything else is a network port.
+    private List<PortDescription> extractPorts(String s) {
+        List<PortDescription> ports = new ArrayList<>();
+
+        if (s.length() == 0) {
+            return ports;
+        }
+
+        Arrays.stream(s.split("\"\"")).forEach(p -> {
+            if (p.contains(EIGHTFIFTY)) {
+                PortDescription cltPort = oduCltPortDescription(
+                        PortNumber.portNumber(ports.size() + 1),
+                        true,
+                        CltSignalType.CLT_10GBE,
+                        extractAnnotations(p));
+                ports.add(cltPort);
+            } else {
+                PortDescription netPort = ochPortDescription(
+                        PortNumber.portNumber(ports.size() + 1),
+                        true,
+                        OduSignalType.ODU2e,
+                        true,
+                        new OchSignal(GridType.DWDM, ChannelSpacing.CHL_50GHZ, 0, 4),
+                        extractAnnotations(p));
+                ports.add(netPort);
+            }
+        });
+
+        return ports;
+    }
+
+    // Unused but provided here for convenience.
+    private void logout() {
+        DeviceId deviceId = handler().data().deviceId();
+        Tl1Controller ctrl = checkNotNull(handler().get(Tl1Controller.class));
+
+        Optional<Tl1Device> device = ctrl.getDevice(deviceId);
+        if (!device.isPresent()) {
+            return;
+        }
+
+        // Logout command
+        Tl1Command logoutCmd = DefaultTl1Command.builder()
+                .withVerb(CANC)
+                .withModifier(USER)
+                .withAid(device.get().username())
+                .withCtag(103)
+                .build();
+        Future<String> logout = ctrl.sendMsg(deviceId, logoutCmd);
+
+        try {
+            String logoutResponse = logout.get(TIMEOUT, TimeUnit.MILLISECONDS);
+
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("Lougout failed", e);
+        }
+    }
+}
diff --git a/drivers/lumentum/src/main/resources/lumentum-drivers.xml b/drivers/lumentum/src/main/resources/lumentum-drivers.xml
index bb4b87b..230cc34 100644
--- a/drivers/lumentum/src/main/resources/lumentum-drivers.xml
+++ b/drivers/lumentum/src/main/resources/lumentum-drivers.xml
@@ -15,17 +15,23 @@
   ~ limitations under the License.
   -->
 <drivers>
-    <driver name="lumentum" manufacturer="Lumentum" hwVersion="SDN ROADM" swVersion="1.0">
+    <driver name="lumentum-sdn" manufacturer="Lumentum" hwVersion="SDN ROADM" swVersion="1.0">
         <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
-                   impl="org.onosproject.drivers.lumentum.LumentumRoadmDeviceDescription"/>
+                   impl="org.onosproject.drivers.lumentum.LumentumRoadmDiscovery"/>
         <behaviour api="org.onosproject.net.behaviour.LambdaQuery"
-                   impl="org.onosproject.drivers.lumentum.LambdaQueryLumentumRoadm"/>
+                   impl="org.onosproject.drivers.lumentum.LumentumSdnRoadmLambdaQuery"/>
         <behaviour api="org.onosproject.net.flow.FlowRuleProgrammable"
-                   impl="org.onosproject.drivers.lumentum.LumentumFlowRuleProgrammable"/>
+                   impl="org.onosproject.drivers.lumentum.LumentumSdnRoadmFlowRuleProgrammable"/>
         <behaviour api="org.onosproject.incubator.net.faultmanagement.alarm.AlarmConsumer"
                    impl="org.onosproject.drivers.lumentum.LumentumAlarmConsumer"/>
         <behaviour api="org.onosproject.net.optical.OpticalDevice"
                    impl="org.onosproject.net.optical.DefaultOpticalDevice"/>
     </driver>
+    <driver name="lumentum-waveready" manufacturer="Lumentum" hwVersion="WR*">
+        <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
+                   impl="org.onosproject.drivers.lumentum.LumentumWaveReadyDiscovery"/>
+        <behaviour api="org.onosproject.net.optical.OpticalDevice"
+                   impl="org.onosproject.net.optical.DefaultOpticalDevice"/>
+    </driver>
 </drivers>
 
diff --git a/drivers/lumentum/src/test/java/org/onosproject/drivers/lumentum/LumentumDriversLoaderTest.java b/drivers/lumentum/src/test/java/org/onosproject/drivers/lumentum/LumentumDriversLoaderTest.java
deleted file mode 100644
index 1723337..0000000
--- a/drivers/lumentum/src/test/java/org/onosproject/drivers/lumentum/LumentumDriversLoaderTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.onosproject.drivers.lumentum;
-
-import org.junit.Before;
-import org.onosproject.net.driver.AbstractDriverLoaderTest;
-
-/**
- * Lumentum drivers loader test.
- */
-public class LumentumDriversLoaderTest extends AbstractDriverLoaderTest {
-
-    @Before
-    public void setUp() {
-        loader = new LumentumDriversLoader();
-    }
-}
diff --git a/modules.defs b/modules.defs
index d4624bc..ed45ad7 100644
--- a/modules.defs
+++ b/modules.defs
@@ -52,6 +52,8 @@
     '//protocols/lisp/api:onos-protocols-lisp-api',
     '//protocols/lisp/ctl:onos-protocols-lisp-ctl',
     '//protocols/lisp/msg:onos-protocols-lisp-msg',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+    '//protocols/tl1/ctl:onos-protocols-tl1-ctl',
 
     '//drivers/utilities:onos-drivers-utilities',
 
@@ -68,6 +70,7 @@
     '//providers/isis/cfg:onos-providers-isis-cfg',
     '//providers/isis/topology:onos-providers-isis-topology',
     '//providers/lisp/device:onos-providers-lisp-device',
+    '//providers/tl1/device:onos-providers-tl1-device',
 
     '//web/api:onos-rest',
     '//web/gui:onos-gui',
@@ -112,6 +115,7 @@
     '//providers/snmp:onos-providers-snmp-oar',
     '//providers/link:onos-providers-link-oar',
     '//providers/lisp:onos-providers-lisp-oar',
+    '//providers/tl1:onos-providers-tl1-oar',
 ]
 
 ONOS_APPS = [
diff --git a/protocols/pom.xml b/protocols/pom.xml
index b0a2ea6..13d44fe 100644
--- a/protocols/pom.xml
+++ b/protocols/pom.xml
@@ -43,6 +43,7 @@
         <module>bmv2</module>
         <module>lisp</module>
         <module>restconf</module>
+        <module>tl1</module>
     </modules>
 
     <dependencies>
diff --git a/protocols/tl1/api/BUCK b/protocols/tl1/api/BUCK
new file mode 100644
index 0000000..3ce3f98
--- /dev/null
+++ b/protocols/tl1/api/BUCK
@@ -0,0 +1,8 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:netty-transport',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+)
diff --git a/protocols/tl1/api/pom.xml b/protocols/tl1/api/pom.xml
new file mode 100644
index 0000000..bedfc6e
--- /dev/null
+++ b/protocols/tl1/api/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>onos-tl1</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>onos-tl1-api</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-handler</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Command.java b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Command.java
new file mode 100644
index 0000000..02dd4dc
--- /dev/null
+++ b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Command.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1;
+
+import com.google.common.annotations.Beta;
+
+import java.util.Optional;
+
+/**
+ * Representation of a TL1 command, which is sent from the controller to a network element.
+ *
+ * The following shows the typical TL1 command structure:
+ *      {@literal VERB-MODIFIER:<tid>:<aid>:<ctag>::parameter-list;}
+ *
+ * The ctag must be a non-zero decimal number consisting of not more than six characters,
+ * and is assumed to be a unique message identifier per device.
+ */
+@Beta
+public interface Tl1Command {
+
+    /**
+     * Minimum CTAG value.
+     */
+    int MIN_CTAG = 0;
+
+    /**
+     * Maximum CTAG value.
+     */
+    int MAX_CTAG = 999999;
+
+    /**
+     * Returns the verb of the command.
+     *
+     * @return the verb
+     */
+    String verb();
+
+    /**
+     * Returns the modifier of the command.
+     *
+     * @return the modifier
+     */
+    String modifier();
+
+    /**
+     * Returns the optional target identifier (tid).
+     *
+     * @return the tid
+     */
+    Optional<String> tid();
+
+    /**
+     * Returns the optional access identifier (aid).
+     *
+     * @return the aid
+     */
+    Optional<String> aid();
+
+    /**
+     * Returns the correlation tag (ctag).
+     *
+     * @return correlation tag
+     */
+    int ctag();
+
+    /**
+     * Returns the optional parameters.
+     *
+     * @return the parameters
+     */
+    Optional<String> parameters();
+
+    /**
+     * TL1 command builder.
+     *
+     * @param <T> builder implementation type
+     */
+    interface Builder<T extends Builder<T>> {
+        /**
+         * Assigns a verb to this TL1 command.
+         *
+         * @param verb a verb
+         * @return this
+         */
+        T withVerb(String verb);
+
+        /**
+         * Assigns a modifier to this TL1 command.
+         *
+         * @param modifier a modifier
+         * @return this
+         */
+        T withModifier(String modifier);
+
+        /**
+         * Assigns a target identifier to this TL1 command.
+         *
+         * @param tid a tid
+         * @return this
+         */
+        T forTid(String tid);
+
+        /**
+         * Assigns an access identifier to this TL1 command.
+         *
+         * @param aid an aid
+         * @return this
+         */
+        T withAid(String aid);
+
+        /**
+         * Assigns a correlation tag to this TL1 command.
+         *
+         * @param ctag a ctag
+         * @return this
+         */
+        T withCtag(int ctag);
+
+        /**
+         * Assigns parameters to this TL1 command.
+         *
+         * @param parameters the parameters
+         * @return this
+         */
+        T withParameters(String parameters);
+
+        /**
+         * Builds a TL1 command.
+         *
+         * @return the TL1 command
+         */
+        Tl1Command build();
+    }
+}
diff --git a/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Controller.java b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Controller.java
new file mode 100644
index 0000000..feaa0dc0
--- /dev/null
+++ b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Controller.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+@Beta
+public interface Tl1Controller {
+    /**
+     * Sends the message to the device asynchronously.
+     *
+     * @param deviceId the device to write to
+     * @param msg the message to write
+     * @return the device response
+     */
+    // TODO: return CompletableFuture<Tl1Message> once we have appropriate builders
+    CompletableFuture<String> sendMsg(DeviceId deviceId, Tl1Command msg);
+
+    /**
+     * Returns the device identified by given ID.
+     *
+     * @param deviceId the device ID to lookup
+     * @return optional Tl1Device
+     */
+    Optional<Tl1Device> getDevice(DeviceId deviceId);
+
+    /**
+    /**
+     * Adds a device to the controller.
+     * @param deviceId the device ID to add
+     * @param device the device to add
+     * @return true if device added, false if already known
+     */
+    boolean addDevice(DeviceId deviceId, Tl1Device device);
+
+    /**
+     * Disconnects the device and removes it from the controller.
+     * @param deviceId the device to remove
+     */
+    void removeDevice(DeviceId deviceId);
+
+    /**
+     * Connects the controller to the device.
+     * @param deviceId the device to disconnect to
+     */
+    void connectDevice(DeviceId deviceId);
+
+    /**
+     * Disconnects the device from the controller.
+     * @param deviceId the device to disconnect from
+     */
+    void disconnectDevice(DeviceId deviceId);
+
+    /**
+     * Returns a set of all devices IDs for this TL1 controller.
+     * @return set of device IDs
+     */
+    Set<DeviceId> getDeviceIds();
+
+    /**
+     * Returns a set of all devices for this TL1 controller.
+     * @return collection of TL1 devices
+     */
+    Collection<Tl1Device> getDevices();
+
+    /**
+     * Registers a listener for TL1 events.
+     *
+     * @param listener the listener to notify
+     */
+    void addListener(Tl1Listener listener);
+
+    /**
+     * Unregisters a listener for TL1 events.
+     *
+     * @param listener the listener to unregister
+     */
+    void removeListener(Tl1Listener listener);
+}
diff --git a/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Device.java b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Device.java
new file mode 100644
index 0000000..71dc01e
--- /dev/null
+++ b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Device.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1;
+
+import com.google.common.annotations.Beta;
+import io.netty.channel.Channel;
+import org.onlab.packet.IpAddress;
+
+@Beta
+public interface Tl1Device {
+    IpAddress ip();
+
+    int port();
+
+    /**
+     * The username to log in to the switch.
+     *
+     * @return the user name
+     */
+    String username();
+
+    /**
+     * The password to log in to the switch.
+     *
+     * @return the password
+     */
+    String password();
+
+    /**
+     * The target identifier (TID) of the device.
+     *
+     * @return the tid
+     */
+    String tid();
+
+    /**
+     * Check if the switch is connected.
+     *
+     * @return true if connected, false otherwise
+     */
+    boolean isConnected();
+
+    /**
+     * Returns the netty channel of the switch.
+     *
+     * @return the netty channel, null if disconnected
+     */
+    Channel channel();
+
+    /**
+     * Connects the switch to the channel.
+     * @param channel the channel
+     */
+    void connect(Channel channel);
+
+    /**
+     * Disconnects the switch from its channel.
+     */
+    void disconnect();
+}
diff --git a/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Listener.java b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Listener.java
new file mode 100644
index 0000000..8260e05
--- /dev/null
+++ b/protocols/tl1/api/src/main/java/org/onosproject/tl1/Tl1Listener.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
+
+@Beta
+public interface Tl1Listener {
+    /**
+     * Notify that the device has connected.
+     *
+     * @param deviceId the connected device
+     */
+    void deviceConnected(DeviceId deviceId);
+
+    /**
+     * Notify that the device has disconnected.
+     *
+     * @param deviceId the disconnected device
+     */
+    void deviceDisconnected(DeviceId deviceId);
+}
diff --git a/protocols/tl1/api/src/main/java/org/onosproject/tl1/package-info.java b/protocols/tl1/api/src/main/java/org/onosproject/tl1/package-info.java
new file mode 100644
index 0000000..964dcf6
--- /dev/null
+++ b/protocols/tl1/api/src/main/java/org/onosproject/tl1/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * TL1 protocol API.
+ */
+package org.onosproject.tl1;
\ No newline at end of file
diff --git a/protocols/tl1/ctl/BUCK b/protocols/tl1/ctl/BUCK
new file mode 100644
index 0000000..2dc94c2
--- /dev/null
+++ b/protocols/tl1/ctl/BUCK
@@ -0,0 +1,18 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:netty-transport',
+    '//lib:netty-buffer',
+    '//lib:netty-codec',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+    '//utils/osgi:onlab-osgi-tests',
+    '//core/api:onos-api-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/protocols/tl1/ctl/pom.xml b/protocols/tl1/ctl/pom.xml
new file mode 100644
index 0000000..033a4f9
--- /dev/null
+++ b/protocols/tl1/ctl/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>onos-tl1</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>onos-tl1-ctl</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-handler</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java
new file mode 100644
index 0000000..ab55427
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import org.onosproject.tl1.Tl1Command;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default implementation of a TL1 command.
+ */
+public final class DefaultTl1Command implements Tl1Command {
+    private static final char HYPHEN = '-';
+    private static final char COLON = ':';
+    private static final char SEMICOLON = ';';
+
+    private String verb;
+    private String modifier;
+    private Optional<String> tid;
+    private String aid;
+    private int ctag;
+    private String parameters;
+
+    private DefaultTl1Command(String verb, String modifier, String tid, String aid, int ctag, String parameters) {
+        this.verb = verb;
+        this.modifier = modifier;
+        this.tid = Optional.ofNullable(tid);
+        this.aid = aid;
+        this.ctag = ctag;
+        this.parameters = parameters;
+    }
+
+    @Override
+    public String verb() {
+        return verb;
+    }
+
+    @Override
+    public String modifier() {
+        return modifier;
+    }
+
+    @Override
+    public Optional<String> tid() {
+        return tid;
+    }
+
+    @Override
+    public Optional<String> aid() {
+        return Optional.ofNullable(aid);
+    }
+
+    @Override
+    public int ctag() {
+        return ctag;
+    }
+
+    @Override
+    public Optional<String> parameters() {
+        return Optional.ofNullable(parameters);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder()
+                .append(verb).append(HYPHEN)
+                .append(modifier).append(COLON)
+                .append(tid().orElse("")).append(COLON)
+                .append(aid().orElse("")).append(COLON)
+                .append(ctag);
+
+        if (parameters().isPresent()) {
+            sb.append(COLON).append(COLON).append(parameters);
+        }
+
+        return sb.append(SEMICOLON).toString();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder implements Tl1Command.Builder {
+        private String verb;
+        private String modifier;
+        private String tid;
+        private String aid;
+        private int ctag;
+        private String parameters;
+
+        @Override
+        public Tl1Command.Builder withVerb(String verb) {
+            this.verb = verb;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withModifier(String modifier) {
+            this.modifier = modifier;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder forTid(String tid) {
+            this.tid = tid;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withAid(String aid) {
+            this.aid = aid;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withCtag(int ctag) {
+            this.ctag = ctag;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withParameters(String parameters) {
+            this.parameters = parameters;
+            return this;
+        }
+
+        @Override
+        public Tl1Command build() {
+            checkNotNull(verb, "Must supply a verb");
+            checkNotNull(modifier, "Must supply a modifier");
+
+            checkArgument(MIN_CTAG < ctag, "ctag cannot be less than " + MIN_CTAG);
+            checkArgument(ctag <= MAX_CTAG, "ctag cannot be larger than " + MAX_CTAG);
+
+            return new DefaultTl1Command(verb, modifier, tid, aid, ctag, parameters);
+        }
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java
new file mode 100644
index 0000000..7a2fa8f
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.DelimiterBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.util.CharsetUtil;
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.Tools;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.tl1.Tl1Command;
+import org.onosproject.tl1.Tl1Controller;
+import org.onosproject.tl1.Tl1Device;
+import org.onosproject.tl1.Tl1Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of TL1 controller.
+ *
+ * Handles the connection and input/output for all registered TL1 devices.
+ * Turn on debug logging if you want to see all message I/O.
+ *
+ * Per device, we track commands using a simple ctag-keyed map. This assumes the client is sending out unique ctag's.
+ */
+@Component(immediate = true)
+@Service
+public class DefaultTl1Controller implements Tl1Controller {
+    private final Logger log = LoggerFactory.getLogger(DefaultTl1Controller.class);
+
+    // TL1 message delimiter (semi colon)
+    private static final ByteBuf DELIMITER = Unpooled.copiedBuffer(new char[]{';'}, Charset.defaultCharset());
+    private static final String COMPLD = "COMPLD";
+    private static final String DENY = "DENY";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    private ConcurrentMap<DeviceId, Tl1Device> deviceMap = new ConcurrentHashMap<>();
+    // Key: channel, value: map with key ctag, value: future TL1 msg (ctags are assumed unique per device)
+    private ConcurrentMap<Channel, ConcurrentMap<Integer, CompletableFuture<String>>> msgMap =
+            new ConcurrentHashMap<>();
+    private EventLoopGroup workerGroup = new NioEventLoopGroup();
+    private Set<Tl1Listener> tl1Listeners = new CopyOnWriteArraySet<>();
+    private ExecutorService executor;
+
+    @Activate
+    public void activate() {
+        executor = Executors.newFixedThreadPool(
+                Runtime.getRuntime().availableProcessors(),
+                Tools.groupedThreads("onos/tl1controller", "%d", log));
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        executor.shutdown();
+        deviceMap.clear();
+        msgMap.clear();
+        log.info("Stopped");
+    }
+
+    @Override
+    /**
+     * This implementation returns an empty string on failure.
+     */
+    public CompletableFuture<String> sendMsg(DeviceId deviceId, Tl1Command msg) {
+        log.debug("Sending TL1 message to device {}: {}", deviceId, msg);
+
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null || !device.isConnected() || !mastershipService.isLocalMaster(deviceId)) {
+            return CompletableFuture.completedFuture(StringUtils.EMPTY);
+        }
+
+        // Create and store completable future, complete it in the channel handler when we receive a response
+        CompletableFuture<String> future = new CompletableFuture<>();
+        Channel channel = device.channel();
+        if (!msgMap.containsKey(channel)) {
+            return CompletableFuture.completedFuture(StringUtils.EMPTY);
+        }
+        msgMap.get(channel).put(msg.ctag(), future);
+
+        // Write message to channel
+        channel.writeAndFlush(Unpooled.copiedBuffer(msg.toString(), CharsetUtil.UTF_8));
+
+        return future;
+    }
+
+    @Override
+    public Optional<Tl1Device> getDevice(DeviceId deviceId) {
+        return Optional.ofNullable(deviceMap.get(deviceId));
+    }
+
+    @Override
+    public boolean addDevice(DeviceId deviceId, Tl1Device device) {
+        log.debug("Adding TL1 device {} {}", deviceId);
+
+        // Ignore if device already known
+        if (deviceMap.containsKey(deviceId)) {
+            log.error("Ignoring duplicate device {}", deviceId);
+            return false;
+        }
+
+        deviceMap.put(deviceId, device);
+        return true;
+    }
+
+    @Override
+    public void connectDevice(DeviceId deviceId) {
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null || device.isConnected()) {
+            return;
+        }
+
+        Bootstrap b = new Bootstrap();
+        b.group(workerGroup)
+                .channel(NioSocketChannel.class)
+                .option(ChannelOption.SO_KEEPALIVE, true)
+                .handler(new ChannelInitializer<SocketChannel>() {
+                    @Override
+                    protected void initChannel(SocketChannel socketChannel) throws Exception {
+                        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(8192, DELIMITER));
+                        socketChannel.pipeline().addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));
+                        // TODO
+                        //socketChannel.pipeline().addLast(new Tl1Decoder());
+                        socketChannel.pipeline().addLast(new Tl1InboundHandler());
+                    }
+                })
+                .remoteAddress(device.ip().toInetAddress(), device.port())
+                .connect()
+                .addListener((ChannelFuture channelFuture) -> {
+                    if (channelFuture.isSuccess()) {
+                        msgMap.put(channelFuture.channel(), new ConcurrentHashMap<>());
+                        device.connect(channelFuture.channel());
+                        tl1Listeners.forEach(l -> executor.execute(() -> l.deviceConnected(deviceId)));
+                    }
+                });
+    }
+
+    @Override
+    public void removeDevice(DeviceId deviceId) {
+        disconnectDevice(deviceId);
+        deviceMap.remove(deviceId);
+    }
+
+    @Override
+    public void addListener(Tl1Listener listener) {
+        tl1Listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(Tl1Listener listener) {
+        tl1Listeners.remove(listener);
+    }
+
+    @Override
+    public void disconnectDevice(DeviceId deviceId) {
+        // Ignore if unknown device
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null) {
+            return;
+        }
+
+        Channel channel = device.channel();
+        if (channel != null) {
+            channel.close();
+        }
+
+        msgMap.remove(channel);
+        device.disconnect();
+        tl1Listeners.forEach(l -> l.deviceDisconnected(deviceId));
+    }
+
+    @Override
+    public Set<DeviceId> getDeviceIds() {
+        return deviceMap.keySet();
+    }
+
+    @Override
+    public Collection<Tl1Device> getDevices() {
+        return deviceMap.values();
+    }
+
+    /**
+     * Crude filtering handler that will only complete our stored future upon receiving a TL1 response messages.
+     */
+    private class Tl1InboundHandler extends SimpleChannelInboundHandler<String> {
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
+            log.debug("Received TL1 message {}", s);
+
+            // Search for "COMPLD" or "DENY" to identify a TL1 response,
+            // then return the remainder of the string.
+            String[] words = s.split("\\s");
+            for (int i = 0; i < words.length; i++) {
+                String w = words[i];
+                if (w.startsWith(COMPLD) || w.startsWith(DENY)) {
+                    // ctag is just in front of it
+                    int ctag = Integer.parseInt(words[i - 1]);
+                    // We return everything that follows to the caller (this will lose line breaks and such)
+                    String result = Arrays.stream(words).skip(i + 1).collect(Collectors.joining());
+                    // Set future when command is executed, good or bad
+                    Map<Integer, CompletableFuture<String>> msg = msgMap.get(ctx.channel());
+                    if (msg != null) {
+                        CompletableFuture<String> f = msg.remove(ctag);
+                        if (f != null) {
+                            f.complete(result);
+                        }
+                    }
+
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java
new file mode 100644
index 0000000..602d24c
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import io.netty.channel.Channel;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.tl1.Tl1Device;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default implementation of a TL1 device.
+ */
+public class DefaultTl1Device implements Tl1Device {
+    private final Logger log = LoggerFactory.getLogger(DefaultTl1Device.class);
+    private static final String TL1 = "tl1";
+
+    private IpAddress ip;
+    private int port;
+    private String username;
+    private String password;
+    private String tid;
+    private Channel channel;
+
+    @Override
+    public void connect(Channel channel) {
+        this.channel = channel;
+    }
+
+    @Override
+    public void disconnect() {
+        this.channel = null;
+    }
+
+    @Override
+    public boolean isConnected() {
+        return channel != null;
+    }
+
+    @Override
+    public IpAddress ip() {
+        return ip;
+    }
+
+    @Override
+    public int port() {
+        return port;
+    }
+
+    @Override
+    public String username() {
+        return username;
+    }
+
+    @Override
+    public String password() {
+        return password;
+    }
+
+    @Override
+    public Channel channel() {
+        return channel;
+    }
+
+    @Override
+    public String tid() {
+        return tid;
+    }
+
+    public DefaultTl1Device(IpAddress ip, int port, String username, String password) {
+        this.ip = checkNotNull(ip);
+        checkArgument((TpPort.MIN_PORT <= port) && (port <= TpPort.MAX_PORT));
+        this.port = port;
+        this.username = checkNotNull(username);
+        this.password = checkNotNull(password);
+        this.tid = null;
+        channel = null;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ip, port, username, password);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DefaultTl1Device) {
+            DefaultTl1Device that = (DefaultTl1Device) obj;
+            return Objects.equals(ip, that.ip) &&
+                    Objects.equals(port, that.port) &&
+                    Objects.equals(username, that.username) &&
+                    Objects.equals(password, that.password) &&
+                    Objects.equals(tid, that.tid) &&
+                    Objects.equals(channel, that.channel);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("ip", ip)
+                .add("port", port)
+                .add("username", username)
+                .add("password", password)
+                .add("tid", tid == null ? "N/A" : tid)
+                .toString();
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java
new file mode 100644
index 0000000..4d7f24e
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * TL1 protocol API implementation.
+ */
+package org.onosproject.tl1.impl;
\ No newline at end of file
diff --git a/protocols/tl1/pom.xml b/protocols/tl1/pom.xml
new file mode 100644
index 0000000..dd2c269
--- /dev/null
+++ b/protocols/tl1/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>onos-protocols</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>onos-tl1</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>ctl</module>
+        <module>api</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/providers/pom.xml b/providers/pom.xml
index fe9f201..14e9c73 100644
--- a/providers/pom.xml
+++ b/providers/pom.xml
@@ -51,6 +51,7 @@
         <module>ospf</module>
         <module>link</module>
         <module>ietfte</module>
+        <module>tl1</module>
     </modules>
 
     <dependencies>
diff --git a/providers/tl1/BUCK b/providers/tl1/BUCK
new file mode 100644
index 0000000..f5ebffb
--- /dev/null
+++ b/providers/tl1/BUCK
@@ -0,0 +1,14 @@
+BUNDLES = [
+    '//providers/tl1/device:onos-providers-tl1-device',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+    '//protocols/tl1/ctl:onos-protocols-tl1-ctl',
+]
+
+onos_app (
+    app_name = 'org.onosproject.tl1',
+    title = 'TL1 Provider',
+    category = 'Provider',
+    url = 'http://onosproject.org',
+    included_bundles = BUNDLES,
+    description = 'TL1 protocol southbound providers.',
+)
diff --git a/providers/tl1/app/app.xml b/providers/tl1/app/app.xml
new file mode 100644
index 0000000..537b8f2
--- /dev/null
+++ b/providers/tl1/app/app.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.tl1" origin="ON.Lab" version="${project.version}"
+     category="Provider" title="TL1 Provider"
+     featuresRepo="mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features"
+     features="${project.artifactId}">
+    <description>${project.description}</description>
+    <artifact>mvn:${project.groupId}/onos-tl1-api/${project.version}</artifact>
+    <artifact>mvn:${project.groupId}/onos-tl1-ctl/${project.version}</artifact>
+
+    <artifact>mvn:${project.groupId}/onos-tl1-provider-device/${project.version}</artifact>
+
+</app>
diff --git a/providers/tl1/app/features.xml b/providers/tl1/app/features.xml
new file mode 100644
index 0000000..114dcd1
--- /dev/null
+++ b/providers/tl1/app/features.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${project.version}">
+    <feature name="${project.artifactId}" version="${project.version}"
+             description="${project.description}">
+        <feature>onos-api</feature>
+        <bundle>mvn:${project.groupId}/onos-tl1-api/${project.version}</bundle>
+        <bundle>mvn:${project.groupId}/onos-tl1-ctl/${project.version}</bundle>
+
+        <bundle>mvn:${project.groupId}/onos-tl1-provider-device/${project.version}</bundle>
+    </feature>
+</features>
+
diff --git a/providers/tl1/app/pom.xml b/providers/tl1/app/pom.xml
new file mode 100644
index 0000000..cee95d4
--- /dev/null
+++ b/providers/tl1/app/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-tl1-providers</artifactId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-tl1-app</artifactId>
+    <packaging>bundle</packaging>
+
+    <properties>
+        <onos.app.name>org.onosproject.tl1</onos.app.name>
+        <onos.app.title>TL1 Meta App</onos.app.title>
+        <onos.app.category>Provider</onos.app.category>
+    </properties>
+
+    <description>TL1 protocol southbound providers</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-provider-device</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/providers/tl1/device/BUCK b/providers/tl1/device/BUCK
new file mode 100644
index 0000000..dddcc78
--- /dev/null
+++ b/providers/tl1/device/BUCK
@@ -0,0 +1,15 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//incubator/api:onos-incubator-api',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+    '//protocols/tl1/ctl:onos-protocols-tl1-ctl',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/providers/tl1/device/example/tl1.json b/providers/tl1/device/example/tl1.json
new file mode 100644
index 0000000..9b1e9f7
--- /dev/null
+++ b/providers/tl1/device/example/tl1.json
@@ -0,0 +1,22 @@
+{
+  "devices": {
+    "tl1:10.128.14.81:3082": {
+      "basic": {
+        "name": "Lumentum",
+        "driver": "lumentum-waveready"
+      }
+    }
+  },
+  "apps": {
+    "org.onosproject.tl1": {
+      "devices": [
+        {
+          "ip": "10.128.14.81",
+          "port": 3082,
+          "username": "test",
+          "password": "test"
+        }
+      ]
+    }
+  }
+}
diff --git a/providers/tl1/device/pom.xml b/providers/tl1/device/pom.xml
new file mode 100644
index 0000000..360381d
--- /dev/null
+++ b/providers/tl1/device/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-tl1-providers</artifactId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-tl1-provider-device</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS TL1 protocol device provider</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-ctl</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1DeviceProvider.java b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1DeviceProvider.java
new file mode 100644
index 0000000..7e98496
--- /dev/null
+++ b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1DeviceProvider.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.provider.tl1.device.impl;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.ChassisId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.config.basics.ConfigException;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.SparseAnnotations;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceDescriptionDiscovery;
+import org.onosproject.net.device.DeviceProvider;
+import org.onosproject.net.device.DeviceProviderRegistry;
+import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.provider.AbstractProvider;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.tl1.Tl1Controller;
+import org.onosproject.tl1.Tl1Device;
+import org.onosproject.tl1.Tl1Listener;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.NoSuchElementException;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Device provider for TL1 devices.
+ *
+ * Sits between ONOS provider service and the TL1 controller.
+ * Relies on network config subsystem to know about devices.
+ */
+@Component(immediate = true)
+public class Tl1DeviceProvider extends AbstractProvider implements DeviceProvider {
+    private static final String APP_NAME = "org.onosproject.tl1";
+    private static final String TL1 = "tl1";
+    private static final String PROVIDER = "org.onosproject.provider.tl1.device";
+    private static final String UNKNOWN = "unknown";
+    private static final int REACHABILITY_TIMEOUT = 2000;      // in milliseconds
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry cfgRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceProviderRegistry providerRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceAdminService deviceAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected Tl1Controller controller;
+
+    private ApplicationId appId;
+    private NetworkConfigListener cfgListener = new InnerConfigListener();
+    private Tl1Listener tl1Listener = new InnerTl1Listener();
+    private DeviceProviderService providerService;
+
+    private final ConfigFactory cfgFactory =
+            new ConfigFactory<ApplicationId, Tl1ProviderConfig>(APP_SUBJECT_FACTORY,
+                    Tl1ProviderConfig.class,
+                    "devices",
+                    true) {
+                @Override
+                public Tl1ProviderConfig createConfig() {
+                    return new Tl1ProviderConfig();
+                }
+            };
+
+    @Activate
+    public void activate() {
+        appId = coreService.registerApplication(APP_NAME);
+        providerService = providerRegistry.register(this);
+        cfgRegistry.addListener(cfgListener);
+        controller.addListener(tl1Listener);
+        cfgRegistry.registerConfigFactory(cfgFactory);
+        registerDevices();
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        controller.removeListener(tl1Listener);
+        cfgRegistry.removeListener(cfgListener);
+        controller.getDeviceIds().forEach(deviceId -> {
+            controller.removeDevice(deviceId);
+            deviceAdminService.removeDevice(deviceId);
+        });
+        providerRegistry.unregister(this);
+        cfgRegistry.unregisterConfigFactory(cfgFactory);
+        providerService = null;
+        log.info("Stopped");
+    }
+
+    public Tl1DeviceProvider() {
+        super(new ProviderId(TL1, PROVIDER));
+    }
+
+    public void triggerProbe(DeviceId deviceId) {
+        // TODO
+    }
+
+    public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
+        switch (newRole) {
+            case MASTER:
+                controller.connectDevice(deviceId);
+                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.MASTER);
+                log.debug("Accepting mastership role change to {} for device {}", newRole, deviceId);
+                break;
+            case STANDBY:
+                controller.disconnectDevice(deviceId);
+                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.STANDBY);
+                break;
+            case NONE:
+                controller.disconnectDevice(deviceId);
+                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.NONE);
+                break;
+            default:
+                log.error("Invalid mastership state: {}", newRole);
+        }
+    }
+
+    // Assumes device is registered in TL1 controller.
+    public boolean isReachable(DeviceId deviceId) {
+        try {
+            // First check if device is already connected.
+            // If not, try to open a socket.
+            Tl1Device device = controller.getDevice(deviceId).get();
+            if (device.isConnected()) {
+                return true;
+            }
+
+            Socket socket = new Socket();
+            socket.connect(new InetSocketAddress(device.ip().toInetAddress(), device.port()), REACHABILITY_TIMEOUT);
+            socket.close();
+            return true;
+        } catch (NoSuchElementException | IOException | IllegalArgumentException e) {
+            log.error("Cannot reach device {}", deviceId, e);
+            return false;
+        }
+    }
+
+    public void changePortState(DeviceId deviceId, PortNumber portNumber, boolean enable) {
+        // TODO
+    }
+
+    // Register all devices in the core and in the TL1 controller
+    void registerDevices() {
+        Tl1ProviderConfig cfg = cfgRegistry.getConfig(appId, Tl1ProviderConfig.class);
+
+        if (cfg == null) {
+            return;
+        }
+
+        try {
+            cfg.readDevices().forEach(device -> {
+                try {
+                    // Add device to TL1 controller
+                    DeviceId deviceId = DeviceId.deviceId(
+                            new URI(TL1, device.ip() + ":" + device.port(), null));
+
+                    if (controller.addDevice(deviceId, device)) {
+                        SparseAnnotations ann = DefaultAnnotations.builder()
+                                .set(AnnotationKeys.PROTOCOL, TL1.toUpperCase())
+                                .build();
+                        // Register device in the core with default parameters and mark it as unavailable
+                        DeviceDescription dd = new DefaultDeviceDescription(deviceId.uri(), Device.Type.SWITCH, UNKNOWN,
+                                UNKNOWN, UNKNOWN, UNKNOWN, new ChassisId(), false, ann);
+                        providerService.deviceConnected(deviceId, dd);
+                    }
+                } catch (URISyntaxException e) {
+                    log.error("Skipping device {}", device, e);
+                }
+            });
+        } catch (ConfigException e) {
+            log.error("Cannot parse network configuration", e);
+        }
+    }
+
+    /**
+     * Tries to update the device and port descriptions through the {@code DeviceDescriptionDiscovery} behaviour.
+     *
+     * @param deviceId the device
+     */
+    void updateDevice(DeviceId deviceId) {
+        Device device = deviceService.getDevice(deviceId);
+
+        if (!device.is(DeviceDescriptionDiscovery.class)) {
+            return;
+        }
+
+        try {
+            // Update device description
+            DeviceDescriptionDiscovery discovery = device.as(DeviceDescriptionDiscovery.class);
+            DeviceDescription dd = discovery.discoverDeviceDetails();
+            if (dd == null) {
+                return;
+            }
+            providerService.deviceConnected(deviceId,
+                    new DefaultDeviceDescription(dd, true, dd.annotations()));
+            // Update ports
+            providerService.updatePorts(deviceId, discovery.discoverPortDetails());
+        } catch (IllegalStateException | IllegalArgumentException e) {
+            log.error("Cannot update device description {}", deviceId, e);
+        }
+    }
+
+    /**
+     * Listener for network configuration events.
+     */
+    private class InnerConfigListener implements NetworkConfigListener {
+        public void event(NetworkConfigEvent event) {
+            if (event.type() == NetworkConfigEvent.Type.CONFIG_ADDED) {
+                registerDevices();
+            } else if (event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED) {
+                // TODO: calculate delta
+                registerDevices();
+            } else if (event.type() == NetworkConfigEvent.Type.CONFIG_REMOVED) {
+                controller.getDeviceIds().forEach(deviceId -> {
+                    controller.removeDevice(deviceId);
+                    deviceAdminService.removeDevice(deviceId);
+                });
+            }
+        }
+
+        public boolean isRelevant(NetworkConfigEvent event) {
+            return event.configClass().equals(Tl1ProviderConfig.class) &&
+                    (event.type() == NetworkConfigEvent.Type.CONFIG_ADDED ||
+                            event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED ||
+                            event.type() == NetworkConfigEvent.Type.CONFIG_REMOVED);
+        }
+    }
+
+    /**
+     * Listener for TL1 events.
+     */
+    private class InnerTl1Listener implements Tl1Listener {
+        @Override
+        public void deviceConnected(DeviceId deviceId) {
+            updateDevice(deviceId);
+        }
+
+        @Override
+        public void deviceDisconnected(DeviceId deviceId) {
+            providerService.deviceDisconnected(deviceId);
+        }
+    }
+}
diff --git a/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1ProviderConfig.java b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1ProviderConfig.java
new file mode 100644
index 0000000..a1db864
--- /dev/null
+++ b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/Tl1ProviderConfig.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.provider.tl1.device.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Sets;
+import org.onlab.packet.IpAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.incubator.net.config.basics.ConfigException;
+import org.onosproject.net.config.Config;
+import org.onosproject.tl1.impl.DefaultTl1Device;
+import org.onosproject.tl1.Tl1Device;
+
+import java.util.Set;
+
+/**
+ * Configuration for TL1 provider.
+ */
+public class Tl1ProviderConfig extends Config<ApplicationId> {
+    public static final String CONFIG_VALUE_ERROR = "Error parsing config value";
+    private static final String IP = "ip";
+    private static final String PORT = "port";
+    private static final String USERNAME = "username";
+    private static final String PASSWORD = "password";
+
+    Set<Tl1Device> readDevices() throws ConfigException {
+        Set<Tl1Device> devices = Sets.newHashSet();
+
+        try {
+            for (JsonNode node : array) {
+                String ip = node.path(IP).asText();
+                IpAddress ipAddress = IpAddress.valueOf(ip);
+                int port = node.path(PORT).asInt();
+                String username = node.path(USERNAME).asText();
+                String password = node.path(PASSWORD).asText();
+                devices.add(new DefaultTl1Device(ipAddress, port, username, password));
+            }
+        } catch (IllegalArgumentException e) {
+            throw new ConfigException(CONFIG_VALUE_ERROR, e);
+        }
+
+        return devices;
+    }
+}
diff --git a/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/package-info.java b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/package-info.java
new file mode 100644
index 0000000..7ba6ea7
--- /dev/null
+++ b/providers/tl1/device/src/main/java/org/onosproject/provider/tl1/device/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * TL1 device provider implementation.
+ */
+package org.onosproject.provider.tl1.device.impl;
\ No newline at end of file
diff --git a/providers/tl1/pom.xml b/providers/tl1/pom.xml
new file mode 100644
index 0000000..00325f4
--- /dev/null
+++ b/providers/tl1/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-providers</artifactId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-tl1-providers</artifactId>
+    <packaging>pom</packaging>
+
+    <description>ONOS TL1 protocol adaptors</description>
+
+    <modules>
+        <module>app</module>
+        <module>device</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file