368 lines
13 KiB
Java
368 lines
13 KiB
Java
|
/*
|
||
|
* Copyright (C) 2007 The Android Open Source Project
|
||
|
*
|
||
|
* 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 android.bluetooth;
|
||
|
|
||
|
import java.util.*;
|
||
|
|
||
|
/**
|
||
|
* An AT (Hayes command) Parser based on (a subset of) the ITU-T V.250 standard.
|
||
|
* <p>
|
||
|
*
|
||
|
* Conformant with the subset of V.250 required for implementation of the
|
||
|
* Bluetooth Headset and Handsfree Profiles, as per Bluetooth SIP
|
||
|
* specifications. Also implements some V.250 features not required by
|
||
|
* Bluetooth - such as chained commands.<p>
|
||
|
*
|
||
|
* Command handlers are registered with an AtParser object. These handlers are
|
||
|
* invoked when command lines are processed by AtParser's process() method.<p>
|
||
|
*
|
||
|
* The AtParser object accepts a new command line to parse via its process()
|
||
|
* method. It breaks each command line into one or more commands. Each command
|
||
|
* is parsed for name, type, and (optional) arguments, and an appropriate
|
||
|
* external handler method is called through the AtCommandHandler interface.
|
||
|
*
|
||
|
* The command types are<ul>
|
||
|
* <li>Basic Command. For example "ATDT1234567890". Basic command names are a
|
||
|
* single character (e.g. "D"), and everything following this character is
|
||
|
* passed to the handler as a string argument (e.g. "T1234567890").
|
||
|
* <li>Action Command. For example "AT+CIMI". The command name is "CIMI", and
|
||
|
* there are no arguments for action commands.
|
||
|
* <li>Read Command. For example "AT+VGM?". The command name is "VGM", and there
|
||
|
* are no arguments for get commands.
|
||
|
* <li>Set Command. For example "AT+VGM=14". The command name is "VGM", and
|
||
|
* there is a single integer argument in this case. In the general case then
|
||
|
* can be zero or more arguments (comma delimited) each of integer or string
|
||
|
* form.
|
||
|
* <li>Test Command. For example "AT+VGM=?. No arguments.
|
||
|
* </ul>
|
||
|
*
|
||
|
* In V.250 the last four command types are known as Extended Commands, and
|
||
|
* they are used heavily in Bluetooth.<p>
|
||
|
*
|
||
|
* Basic commands cannot be chained in this implementation. For Bluetooth
|
||
|
* headset/handsfree use this is acceptable, because they only use the basic
|
||
|
* commands ATA and ATD, which are not allowed to be chained. For general V.250
|
||
|
* use we would need to improve this class to allow Basic command chaining -
|
||
|
* however it's tricky to get right because there is no delimiter for Basic
|
||
|
* command chaining.<p>
|
||
|
*
|
||
|
* Extended commands can be chained. For example:<p>
|
||
|
* AT+VGM?;+VGM=14;+CIMI<p>
|
||
|
* This is equivalent to:<p>
|
||
|
* AT+VGM?
|
||
|
* AT+VGM=14
|
||
|
* AT+CIMI
|
||
|
* Except that only one final result code is return (although several
|
||
|
* intermediate responses may be returned), and as soon as one command in the
|
||
|
* chain fails the rest are abandoned.<p>
|
||
|
*
|
||
|
* Handlers are registered by there command name via register(Char c, ...) or
|
||
|
* register(String s, ...). Handlers for Basic command should be registered by
|
||
|
* the basic command character, and handlers for Extended commands should be
|
||
|
* registered by String.<p>
|
||
|
*
|
||
|
* Refer to:<ul>
|
||
|
* <li>ITU-T Recommendation V.250
|
||
|
* <li>ETSI TS 127.007 (AT Command set for User Equipment, 3GPP TS 27.007)
|
||
|
* <li>Bluetooth Headset Profile Spec (K6)
|
||
|
* <li>Bluetooth Handsfree Profile Spec (HFP 1.5)
|
||
|
* </ul>
|
||
|
* @hide
|
||
|
*/
|
||
|
public class AtParser {
|
||
|
|
||
|
// Extended command type enumeration, only used internally
|
||
|
private static final int TYPE_ACTION = 0; // AT+FOO
|
||
|
private static final int TYPE_READ = 1; // AT+FOO?
|
||
|
private static final int TYPE_SET = 2; // AT+FOO=
|
||
|
private static final int TYPE_TEST = 3; // AT+FOO=?
|
||
|
|
||
|
private HashMap<String, AtCommandHandler> mExtHandlers;
|
||
|
private HashMap<Character, AtCommandHandler> mBasicHandlers;
|
||
|
|
||
|
private String mLastInput; // for "A/" (repeat last command) support
|
||
|
|
||
|
/**
|
||
|
* Create a new AtParser.<p>
|
||
|
* No handlers are registered.
|
||
|
*/
|
||
|
public AtParser() {
|
||
|
mBasicHandlers = new HashMap<Character, AtCommandHandler>();
|
||
|
mExtHandlers = new HashMap<String, AtCommandHandler>();
|
||
|
mLastInput = "";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register a Basic command handler.<p>
|
||
|
* Basic command handlers are later called via their
|
||
|
* <code>handleBasicCommand(String args)</code> method.
|
||
|
* @param command Command name - a single character
|
||
|
* @param handler Handler to register
|
||
|
*/
|
||
|
public void register(Character command, AtCommandHandler handler) {
|
||
|
mBasicHandlers.put(command, handler);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register an Extended command handler.<p>
|
||
|
* Extended command handlers are later called via:<ul>
|
||
|
* <li><code>handleActionCommand()</code>
|
||
|
* <li><code>handleGetCommand()</code>
|
||
|
* <li><code>handleSetCommand()</code>
|
||
|
* <li><code>handleTestCommand()</code>
|
||
|
* </ul>
|
||
|
* Only one method will be called for each command processed.
|
||
|
* @param command Command name - can be multiple characters
|
||
|
* @param handler Handler to register
|
||
|
*/
|
||
|
public void register(String command, AtCommandHandler handler) {
|
||
|
mExtHandlers.put(command, handler);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Strip input of whitespace and force Uppercase - except sections inside
|
||
|
* quotes. Also fixes unmatched quotes (by appending a quote). Double
|
||
|
* quotes " are the only quotes allowed by V.250
|
||
|
*/
|
||
|
static private String clean(String input) {
|
||
|
StringBuilder out = new StringBuilder(input.length());
|
||
|
|
||
|
for (int i = 0; i < input.length(); i++) {
|
||
|
char c = input.charAt(i);
|
||
|
if (c == '"') {
|
||
|
int j = input.indexOf('"', i + 1 ); // search for closing "
|
||
|
if (j == -1) { // unmatched ", insert one.
|
||
|
out.append(input.substring(i, input.length()));
|
||
|
out.append('"');
|
||
|
break;
|
||
|
}
|
||
|
out.append(input.substring(i, j + 1));
|
||
|
i = j;
|
||
|
} else if (c != ' ') {
|
||
|
out.append(Character.toUpperCase(c));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return out.toString();
|
||
|
}
|
||
|
|
||
|
static private boolean isAtoZ(char c) {
|
||
|
return (c >= 'A' && c <= 'Z');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Find a character ch, ignoring quoted sections.
|
||
|
* Return input.length() if not found.
|
||
|
*/
|
||
|
static private int findChar(char ch, String input, int fromIndex) {
|
||
|
for (int i = fromIndex; i < input.length(); i++) {
|
||
|
char c = input.charAt(i);
|
||
|
if (c == '"') {
|
||
|
i = input.indexOf('"', i + 1);
|
||
|
if (i == -1) {
|
||
|
return input.length();
|
||
|
}
|
||
|
} else if (c == ch) {
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
return input.length();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Break an argument string into individual arguments (comma delimited).
|
||
|
* Integer arguments are turned into Integer objects. Otherwise a String
|
||
|
* object is used.
|
||
|
*/
|
||
|
static private Object[] generateArgs(String input) {
|
||
|
int i = 0;
|
||
|
int j;
|
||
|
ArrayList<Object> out = new ArrayList<Object>();
|
||
|
while (i <= input.length()) {
|
||
|
j = findChar(',', input, i);
|
||
|
|
||
|
String arg = input.substring(i, j);
|
||
|
try {
|
||
|
out.add(new Integer(arg));
|
||
|
} catch (NumberFormatException e) {
|
||
|
out.add(arg);
|
||
|
}
|
||
|
|
||
|
i = j + 1; // move past comma
|
||
|
}
|
||
|
return out.toArray();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the index of the end of character after the last character in
|
||
|
* the extended command name. Uses the V.250 spec for allowed command
|
||
|
* names.
|
||
|
*/
|
||
|
static private int findEndExtendedName(String input, int index) {
|
||
|
for (int i = index; i < input.length(); i++) {
|
||
|
char c = input.charAt(i);
|
||
|
|
||
|
// V.250 defines the following chars as legal extended command
|
||
|
// names
|
||
|
if (isAtoZ(c)) continue;
|
||
|
if (c >= '0' && c <= '9') continue;
|
||
|
switch (c) {
|
||
|
case '!':
|
||
|
case '%':
|
||
|
case '-':
|
||
|
case '.':
|
||
|
case '/':
|
||
|
case ':':
|
||
|
case '_':
|
||
|
continue;
|
||
|
default:
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
return input.length();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Processes an incoming AT command line.<p>
|
||
|
* This method will invoke zero or one command handler methods for each
|
||
|
* command in the command line.<p>
|
||
|
* @param raw_input The AT input, without EOL delimiter (e.g. <CR>).
|
||
|
* @return Result object for this command line. This can be
|
||
|
* converted to a String[] response with toStrings().
|
||
|
*/
|
||
|
public AtCommandResult process(String raw_input) {
|
||
|
String input = clean(raw_input);
|
||
|
|
||
|
// Handle "A/" (repeat previous line)
|
||
|
if (input.regionMatches(0, "A/", 0, 2)) {
|
||
|
input = new String(mLastInput);
|
||
|
} else {
|
||
|
mLastInput = new String(input);
|
||
|
}
|
||
|
|
||
|
// Handle empty line - no response necessary
|
||
|
if (input.equals("")) {
|
||
|
// Return []
|
||
|
return new AtCommandResult(AtCommandResult.UNSOLICITED);
|
||
|
}
|
||
|
|
||
|
// Anything else deserves an error
|
||
|
if (!input.regionMatches(0, "AT", 0, 2)) {
|
||
|
// Return ["ERROR"]
|
||
|
return new AtCommandResult(AtCommandResult.ERROR);
|
||
|
}
|
||
|
|
||
|
// Ok we have a command that starts with AT. Process it
|
||
|
int index = 2;
|
||
|
AtCommandResult result =
|
||
|
new AtCommandResult(AtCommandResult.OK);
|
||
|
while (index < input.length()) {
|
||
|
char c = input.charAt(index);
|
||
|
|
||
|
if (isAtoZ(c)) {
|
||
|
// Option 1: Basic Command
|
||
|
// Pass the rest of the line as is to the handler. Do not
|
||
|
// look for any more commands on this line.
|
||
|
String args = input.substring(index + 1);
|
||
|
if (mBasicHandlers.containsKey((Character)c)) {
|
||
|
result.addResult(mBasicHandlers.get(
|
||
|
(Character)c).handleBasicCommand(args));
|
||
|
return result;
|
||
|
} else {
|
||
|
// no handler
|
||
|
result.addResult(
|
||
|
new AtCommandResult(AtCommandResult.ERROR));
|
||
|
return result;
|
||
|
}
|
||
|
// control never reaches here
|
||
|
}
|
||
|
|
||
|
if (c == '+') {
|
||
|
// Option 2: Extended Command
|
||
|
// Search for first non-name character. Short-circuit if
|
||
|
// we don't handle this command name.
|
||
|
int i = findEndExtendedName(input, index + 1);
|
||
|
String commandName = input.substring(index, i);
|
||
|
if (!mExtHandlers.containsKey(commandName)) {
|
||
|
// no handler
|
||
|
result.addResult(
|
||
|
new AtCommandResult(AtCommandResult.ERROR));
|
||
|
return result;
|
||
|
}
|
||
|
AtCommandHandler handler = mExtHandlers.get(commandName);
|
||
|
|
||
|
// Search for end of this command - this is usually the end of
|
||
|
// line
|
||
|
int endIndex = findChar(';', input, index);
|
||
|
|
||
|
// Determine what type of command this is.
|
||
|
// Default to TYPE_ACTION if we can't find anything else
|
||
|
// obvious.
|
||
|
int type;
|
||
|
|
||
|
if (i >= endIndex) {
|
||
|
type = TYPE_ACTION;
|
||
|
} else if (input.charAt(i) == '?') {
|
||
|
type = TYPE_READ;
|
||
|
} else if (input.charAt(i) == '=') {
|
||
|
if (i + 1 < endIndex) {
|
||
|
if (input.charAt(i + 1) == '?') {
|
||
|
type = TYPE_TEST;
|
||
|
} else {
|
||
|
type = TYPE_SET;
|
||
|
}
|
||
|
} else {
|
||
|
type = TYPE_SET;
|
||
|
}
|
||
|
} else {
|
||
|
type = TYPE_ACTION;
|
||
|
}
|
||
|
|
||
|
// Call this command. Short-circuit as soon as a command fails
|
||
|
switch (type) {
|
||
|
case TYPE_ACTION:
|
||
|
result.addResult(handler.handleActionCommand());
|
||
|
break;
|
||
|
case TYPE_READ:
|
||
|
result.addResult(handler.handleReadCommand());
|
||
|
break;
|
||
|
case TYPE_TEST:
|
||
|
result.addResult(handler.handleTestCommand());
|
||
|
break;
|
||
|
case TYPE_SET:
|
||
|
Object[] args =
|
||
|
generateArgs(input.substring(i + 1, endIndex));
|
||
|
result.addResult(handler.handleSetCommand(args));
|
||
|
break;
|
||
|
}
|
||
|
if (result.getResultCode() != AtCommandResult.OK) {
|
||
|
return result; // short-circuit
|
||
|
}
|
||
|
|
||
|
index = endIndex;
|
||
|
} else {
|
||
|
// Can't tell if this is a basic or extended command.
|
||
|
// Push forwards and hope we hit something.
|
||
|
index++;
|
||
|
}
|
||
|
}
|
||
|
// Finished processing (and all results were ok)
|
||
|
return result;
|
||
|
}
|
||
|
}
|