diff --git a/src/python/dm/aps_beamline_tools/__init__.py b/src/python/dm/aps_beamline_tools/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6342600419f9dbbf99f06d7b74936545fd108b41
--- /dev/null
+++ b/src/python/dm/aps_beamline_tools/__init__.py
@@ -0,0 +1 @@
+__version__ = "1.1 (2017.03.01)"
diff --git a/src/python/dm/aps_beamline_tools/cli/__init__.py b/src/python/dm/aps_beamline_tools/cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6342600419f9dbbf99f06d7b74936545fd108b41
--- /dev/null
+++ b/src/python/dm/aps_beamline_tools/cli/__init__.py
@@ -0,0 +1 @@
+__version__ = "1.1 (2017.03.01)"
diff --git a/src/python/dm/aps_beamline_tools/cli/apsBeamlineCli.py b/src/python/dm/aps_beamline_tools/cli/apsBeamlineCli.py
new file mode 100755
index 0000000000000000000000000000000000000000..ed70f49329d986515a114fbf9930f3ad695d3c49
--- /dev/null
+++ b/src/python/dm/aps_beamline_tools/cli/apsBeamlineCli.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+from dm.common.cli.dmCli import DmCli
+from dm.common.exceptions.invalidRequest import InvalidRequest
+from dm.common.utility.configurationManager import ConfigurationManager
+
+class ApsBeamlineCli(DmCli):
+    """ Base APS beamline cli class. """
+
+    def __init__(self, validArgCount=0):
+        DmCli.__init__(self, validArgCount)
+
+        configManager = ConfigurationManager.getInstance()
+        self.allowedExperimentTypes = configManager.getAllowedExperimentTypes()
+        allowedExperimentTypesHelp = ''
+        if self.allowedExperimentTypes:
+            allowedExperimentTypesHelp = ' Allowed types: %s' % self.allowedExperimentTypes
+        self.stationName = configManager.getStationName()
+        self.beamlineName = configManager.getBeamlineName()
+        self.beamlineManagers = configManager.getBeamlineManagers()
+        self.dsServiceHost = configManager.getDsWebServiceHost()
+        self.dsServicePort = configManager.getDsWebServicePort()
+        self.daqServiceHost = configManager.getDaqWebServiceHost()
+        self.daqServicePort = configManager.getDaqWebServicePort()
+        self.serviceProtocol = configManager.getWebServiceProtocol()
+
+        loginGroup = 'Login Options'
+        self.addOptionGroup(loginGroup, None)
+        self.addOptionToGroup(loginGroup, '', '--login-file', dest='loginFile', help='DM login file, contains "<dm username>|<dm password>" pair. It may be specified using DM_LOGIN_FILE environment variable.')
+        self.addOptionToGroup(loginGroup, '', '--bss-login-file', dest='bssLoginFile', help='BSS login file, contains "<bss username>|<bss password>" pair. It may be specified via environment variable DM_BSS_LOGIN_FILE.')
+
+    def parseArgs(self, usage=None):
+        DmCli.parseArgs(self, usage)
+        (self.loginUsername,self.loginPassword) = self.parseLoginFile(self.getLoginFile())
+        self.bssLoginFile = self.getBssLoginFile()
+        return (self.options, self.args)
+
+    def getLoginFile(self):
+        if not self.options.loginFile:
+            return ConfigurationManager.getInstance().getLoginFile()
+        return self.options.loginFile
+
+    def getBssLoginFile(self):
+        if not self.options.bssLoginFile:
+            return ConfigurationManager.getInstance().getBssLoginFile()
+        return self.options.bssLoginFile
+
+    def getStationName(self):
+        return self.stationName
+
+    def parseLoginFile(self,loginFile):
+        username = None
+        password = None
+        try:
+            # Assume form <username>|<password>
+            if loginFile:
+                tokenList = open(loginFile).readline().split('|')
+                if len(tokenList) == 2:
+                    username = tokenList[0].strip()
+                    password = tokenList[1].strip()
+        except:
+            # Ignore invalid login file
+            pass
+        return (username,password)
+
+    def checkCredentials(self):
+        if not self.hasCredentials():
+            raise InvalidRequest('DM login credentials are not specified.')
+        if not self.hasBssCredentials():
+            raise InvalidRequest('BSS login credentials are not specified.')
+
+    def hasCredentials(self):
+        return (self.loginUsername != None and self.loginPassword != None)
+
+    def hasBssCredentials(self):
+        return self.bssLoginFile != None
+
+#######################################################################
+# Testing
+
+if __name__ == '__main__':
+    pass
diff --git a/src/python/dm/aps_beamline_tools/cli/daqCli.py b/src/python/dm/aps_beamline_tools/cli/daqCli.py
new file mode 100755
index 0000000000000000000000000000000000000000..7d83aee3a89144fda78c2afb415234e058610480
--- /dev/null
+++ b/src/python/dm/aps_beamline_tools/cli/daqCli.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+
+import os
+from dm.aps_bss.api.apsBssApi import ApsBssApi
+from dm.ds_web_service.api.experimentDsApi import ExperimentDsApi
+from dm.ds_web_service.api.userDsApi import UserDsApi
+from dm.daq_web_service.api.experimentDaqApi import ExperimentDaqApi
+
+from dm.common.utility.ftpUtility import FtpUtility
+from dm.common.exceptions.invalidRequest import InvalidRequest
+from dm.common.exceptions.objectNotFound import ObjectNotFound
+from dm.common.utility.configurationManager import ConfigurationManager
+from dm.aps_beamline_tools.cli.apsBeamlineCli import ApsBeamlineCli
+
+class DaqCli(ApsBeamlineCli):
+    def __init__(self, validArgCount=ApsBeamlineCli.ANY_NUMBER_OF_POSITIONAL_ARGS):
+        ApsBeamlineCli.__init__(self, validArgCount)
+        configManager = ConfigurationManager.getInstance()
+        self.allowedExperimentTypes = configManager.getAllowedExperimentTypes()
+        allowedExperimentTypesHelp = ''
+	self.defaultExperimentType = None
+        if self.allowedExperimentTypes:
+            allowedExperimentTypesHelp = ' Allowed types: %s' % self.allowedExperimentTypes
+	    self.defaultExperimentType = self.allowedExperimentTypes.split(',')[0]
+
+        self.addOption('', '--experiment', dest='experimentName', help='Experiment name.')
+        self.addOption('', '--data-directory', dest='dataDirectory', help='Experiment data directory.')
+
+        # Experiment options.
+        expGroup = 'Add/Update Experiment Options'
+        self.addOptionGroup(expGroup, prepend=True)
+        self.addOptionToGroup(expGroup, '', '--type', dest='typeName', default=self.defaultExperimentType, help='Experiment type name.%s' % allowedExperimentTypesHelp)
+        self.addOptionToGroup(expGroup, '', '--description', dest='description', help='Experiment description.')
+        self.addOptionToGroup(expGroup, '', '--start-date', dest='startDate', help='Experiment start date in format DD-MMM-YY.')
+        self.addOptionToGroup(expGroup, '', '--end-date', dest='endDate', help='Experiment end date in format DD-MMM-YY.')
+        self.addOptionToGroup(expGroup, '', '--users', dest='users', help='Comma-separated list of DM usernames to be added to the new experiment as users.')
+        self.addOptionToGroup(expGroup, '', '--proposal-id', dest='proposalId', help='Beamline proposal id. If specified, all users listed on the proposal will be added to the new experiment.')
+        self.addOptionToGroup(expGroup, '', '--run', dest='runName', help='Run name. If not specified, current run name is assumed for beamline proposal.')
+
+        # Daq Options
+        daqGroup = 'DAQ Options'
+        self.addOptionGroup(daqGroup, prepend=True)
+        self.addOptionToGroup(daqGroup, '', '--dest-directory', dest='destDirectory', help='Destination directory relative to experiment root path.')
+        self.addOptionToGroup(daqGroup, '', '--duration', dest='duration', help='DAQ duration; it must be specified in hours (h) or days (d). Examples: "8h", "14d".')
+        self.addOptionToGroup(daqGroup, '', '--upload-data-directory-on-exit', dest='uploadDataDirectoryOnExit', help='Data directory that will be uploaded automatically after DAQ is stopped.')
+        self.addOptionToGroup(daqGroup, '', '--upload-dest-directory-on-exit', dest='uploadDestDirectoryOnExit', help='Destination directory relative to experiment root path for automatic upload after DAQ is stopped. Requires upload data directory to be specified.')
+        self.addOptionToGroup(daqGroup, '', '--process-hidden', dest='processHidden', action='store_true', default=False, help='Process hidden source files.')
+
+    def checkArgs(self):
+        if self.options.experimentName is None:
+            raise InvalidRequest('Experiment name must be provided.')
+        if self.options.dataDirectory is None:
+            raise InvalidRequest('Experiment data directory must be provided.')
+        if self.getTypeName() and self.allowedExperimentTypes:
+            if self.getTypeName() not in self.allowedExperimentTypes.split(','):
+                raise InvalidRequest('Experiment type %s is not allowed on this station. Allowed types are: %s.' % (self.getTypeName(), self.allowedExperimentTypes))
+
+    def updateDaqInfoFromOptions(self, daqInfo):
+        if self.options.processHidden:
+            daqInfo['processHiddenFiles'] = True
+        if self.options.duration:
+            duration = self.options.duration
+            if duration.endswith('h'):
+                daqInfo['maxRunTimeInHours'] = int(duration[0:-1])
+            elif duration.endswith('d'):
+                daqInfo['maxRunTimeInHours'] = int(duration[0:-1])*self.HOURS_PER_DAY
+            else:
+                raise InvalidRequest('Maximum run time must contain valid unit specifier: "h" for hours or "d" for days.')
+        if self.options.destDirectory:
+            daqInfo['destDirectory'] = self.options.destDirectory
+        if self.options.uploadDataDirectoryOnExit:
+            daqInfo['uploadDataDirectoryOnExit'] = self.options.uploadDataDirectoryOnExit
+        if self.options.uploadDestDirectoryOnExit:
+            if not self.options.uploadDataDirectoryOnExit:
+                raise InvalidRequest('Upload destination directory on exit requires that upload data directory is specified as well.')
+            daqInfo['uploadDestDirectoryOnExit'] = self.options.uploadDestDirectoryOnExit
+
+    def getExperimentName(self):
+        return self.options.experimentName
+
+    def getTypeName(self):
+        return self.options.typeName
+
+    def getDescription(self):
+        return self.options.description
+
+    def getStartDate(self):
+        return self.options.startDate
+
+    def getEndDate(self):
+        return self.options.endDate
+
+    def getProposalId(self):
+        proposalId = self.options.proposalId
+        if proposalId:
+            proposalId = int(proposalId)
+        return proposalId
+
+    def getUsers(self):
+        # Return list of users and beamline managers that can access data
+        users = self.options.users
+        if users:
+            users = users.split(',')
+        else:
+            users = []
+        beamlineManagers = self.beamlineManagers
+        if beamlineManagers:
+            beamlineManagers = beamlineManagers.split(',')
+        else:
+            beamlineManagers = []
+        # Remove duplicates by converting into set
+        return list(set(users+beamlineManagers))
+
+    def getDataDirectory(self):
+        dataDirectory = self.options.dataDirectory
+        replacementMap = os.environ.get('DM_DATA_DIRECTORY_MAP', '')
+        (scheme, host, port, dirPath) = FtpUtility.parseUrl(dataDirectory)
+        if dirPath and replacementMap:
+            # Map entries are expected to be in the form
+            # <original>|<replacement>;<original>|<replacement>;...
+            for entry in replacementMap.split(';'):
+                original = entry.split('|')[0]
+                replacement = entry.split('|')[1]
+                dirPath = dataDirectory.replace(original,replacement)
+        return FtpUtility.assembleUrl(scheme, host, port, dirPath)
+
+    def addOrUpdateExperiment(self):
+        dsExperimentApi = ExperimentDsApi(self.loginUsername, self.loginPassword, self.dsServiceHost, self.dsServicePort, self.serviceProtocol)
+        dsUserApi = UserDsApi(self.loginUsername, self.loginPassword, self.dsServiceHost, self.dsServicePort, self.serviceProtocol)
+
+        description = self.getDescription()
+        proposalId = self.getProposalId()
+        experimenters = []
+        if proposalId:
+            bssApi = ApsBssApi(loginFile=self.options.bssLoginFile)
+            proposal = bssApi.getBeamlineProposal(proposalId=proposalId, runName=self.options.runName)
+            experimenters = proposal.get('experimenters', [])
+            if not description:
+                description = '%s (Proposal id: %s)' % (proposal['title'], proposalId)
+
+
+        users = self.getUsers()
+        pis = []
+        for experimenter in experimenters:
+            badge = int(experimenter['badge'])
+            if not badge:
+                #print 'Skipping user %s due to invalid badge.' % lastName 
+                continue
+            username = 'd%s' % badge
+
+            # Clasify user
+            if experimenter.get('piFlag') == 'Y':
+                if not pis.count(username):
+                    pis.append(username)
+                if users.count(username):
+                    users.remove(username)
+            else:
+                if not users.count(username):
+                    users.append(username)
+
+        for username in users+pis:
+            # Check that user exists
+            dsUserApi.getUserByUsername(username)
+           
+
+        # Everything looks good, add experiment
+        try:
+            experiment = dsExperimentApi.getExperimentByName(self.getExperimentName())
+            experimentStation = experiment.get('experimentStation')
+            stationName = ''
+            if experimentStation:
+                stationName = experimentStation.get('name')
+            if stationName != self.getStationName():
+                raise InvalidRequest('Experiment %s already exists for station %s.' % (self.getExperimentName(), stationName))
+        except ObjectNotFound, ex:
+            experiment = dsExperimentApi.addExperiment(self.getExperimentName(), self.getStationName(), self.getTypeName(), description, self.getStartDate(), self.getEndDate())
+
+        # Add pis.
+        experimentUsernameList = experiment.get('experimentUsernameList', [])
+        experimentName = experiment['name']
+        roleName = 'PI'
+        for username in pis:
+            if username not in experimentUsernameList: 
+                dsUserApi.addUserExperimentRole(username, roleName, experimentName)
+        roleName = 'User'
+        for username in users:
+            if username not in experimentUsernameList: 
+                dsUserApi.addUserExperimentRole(username, roleName, experimentName)
+        if len(users+pis):
+            experiment = dsExperimentApi.getExperimentByName(experimentName)
+        return experiment
+
+    def startDaq(self):
+        daqExperimentApi = ExperimentDaqApi(self.loginUsername, self.loginPassword, self.daqServiceHost, self.daqServicePort, self.serviceProtocol)
+        daqInfo = self.splitArgsIntoDict()
+        self.updateDaqInfoFromOptions(daqInfo)
+        daqInfo = daqExperimentApi.startDaq(self.getExperimentName(), self.getDataDirectory(), daqInfo=daqInfo)
+        return daqInfo
+
+    def runCommand(self):
+        self.parseArgs(usage="""
+    dm-%s-daq --experiment=EXPERIMENTNAME --data-directory=DATADIRECTORY
+        [--duration=DURATION]
+        [--dest-directory=DESTDIRECTORY]
+        [--upload-data-directory-on-exit=UPLOADDATADIRECTORYONEXIT]
+        [--upload-dest-directory-on-exit=UPLOADDESTDIRECTORYONEXIT]
+        [--process-hidden]
+        [--type=TYPENAME]
+        [--description=DESCRIPTION] 
+        [--start-date=STARTDATE] 
+        [--end-date=ENDDATE]
+        [--users=USERS]
+        [--proposal-id=PROPOSALID]
+        [--run=RUNNAME]
+        [key1:value1, key2:value2, ...]
+
+Description:
+    Run DAQ for experiment on station %s. If experiment does not exist, it will be
+    added to the DM database. If list of users or proposal id is specified, this command will also add roles for all users listed on the proposal.
+        """ % (self.getStationName().lower(), self.getStationName()))
+        self.checkArgs()
+        self.checkCredentials()
+        experiment = self.addOrUpdateExperiment()
+        print 'EXPERIMENT INFO'
+        print experiment.getDisplayString(self.getDisplayKeys(), self.getDisplayFormat())
+
+        print 
+        daqInfo = self.startDaq()
+        print 'DAQ INFO'
+        print daqInfo.getDisplayString(self.getDisplayKeys(), self.getDisplayFormat())
+
+
+#######################################################################
+# Run command.
+if __name__ == '__main__':
+    cli = DaqCli()
+    cli.run()
+