diff --git a/.gitignore b/.gitignore index 2308f091..06995817 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc __pycache__ bluepy/*.o +bluepy/version.? #bluepy/bluepy-helper # eclipse project related files .settings/ diff --git a/bluepy/Makefile b/bluepy/Makefile index fde174ab..ccc9faa4 100644 --- a/bluepy/Makefile +++ b/bluepy/Makefile @@ -8,13 +8,20 @@ BLUEZ_SRCS += src/shared/io-glib.c src/shared/timeout-glib.c IMPORT_SRCS = $(addprefix $(BLUEZ_PATH)/, $(BLUEZ_SRCS)) LOCAL_SRCS = bluepy-helper.c +VERSION_SRCS = version.c + +IMPORT_OBJS = $(IMPORT_SRCS:.c=.o) +LOCAL_OBJS = $(LOCAL_SRCS:.c=.o) +VERSION_OBJS = $(VERSION_SRCS:.c=.o) CC ?= gcc -CFLAGS += -Os -g -Wall # -Werror +CFLAGS += -g -Wall # -Werror CPPFLAGS += -DHAVE_CONFIG_H ifneq ($(DEBUGGING),) -CFLAGS += -DBLUEPY_DEBUG=1 +CFLAGS += -DBLUEPY_DEBUG=1 -O0 +else +CFLAGS += -Os endif CPPFLAGS += -I$(BLUEZ_PATH)/attrib -I$(BLUEZ_PATH) -I$(BLUEZ_PATH)/lib -I$(BLUEZ_PATH)/src -I$(BLUEZ_PATH)/gdbus -I$(BLUEZ_PATH)/btio -I$(BLUEZ_PATH)/sys @@ -24,8 +31,37 @@ LDLIBS += $(shell pkg-config glib-2.0 --libs) all: bluepy-helper -bluepy-helper: $(LOCAL_SRCS) $(IMPORT_SRCS) - $(CC) -L. $(CFLAGS) $(CPPFLAGS) -o $@ $(LOCAL_SRCS) $(IMPORT_SRCS) $(LDLIBS) +# Generate the "version.c" based on the python package version +# and GIT revision (if available). +# +# The 'grep|sed|tr' extract the version string from the 'setup.py' file. +# The 'git describe' extract the git version (if there is a .git directory). +# The output of the above is a single string like so: +# 1.2.0 (v/1.2.0-16-g3ce360c-dirty) +# The top and bottom printf's generate valid C code, like so: +# const char* bluepy_helper_version = "1.2.0 (v/1.2.0-16-g3ce360c-dirty)"; +version.c: ../setup.py $(LOCAL_SRCS) $(IMPORT_SRCS) + @echo Regenerating $@ + @( \ + printf "const char* bluepy_helper_version = \"" ; \ + \ + grep "version=" ../setup.py \ + | sed -e "s/[^0-9]*\([0-9\.]*\).*/\1/" \ + | tr -d '\n' ; \ + \ + test -d ../.git \ + && { printf " (" ; \ + git describe --always --dirty | tr -d '\n' ; \ + printf ")" ; } ; \ + \ + printf '";\n' ) > $@ + +bluepy-helper: $(LOCAL_OBJS) $(IMPORT_OBJS) $(VERSION_OBJS) + $(CC) -L. -o $@ $^ $(LDLIBS) + +# NOTE: +# make's built-in rule for compiling C to obj files is sufficient +# to build the objects, no need for explicit rule. $(IMPORT_SRCS): bluez-src.tgz tar xzf $< @@ -45,7 +81,7 @@ TAGS: *.c $(BLUEZ_PATH)/attrib/*.[ch] $(BLUEZ_PATH)/btio/*.[ch] etags $^ clean: - rm -rf *.o bluepy-helper TAGS $(BLUEZ_PATH) + rm -rf $(VERSION_OBJS) $(VERSION_SRCS) $(LOCAL_OBJS) $(IMPORT_OBJS) bluepy-helper TAGS $(BLUEZ_PATH) diff --git a/bluepy/bluepy-helper.c b/bluepy/bluepy-helper.c index 88b7ff4d..a46e52a3 100644 --- a/bluepy/bluepy-helper.c +++ b/bluepy/bluepy-helper.c @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include @@ -67,6 +69,9 @@ static void try_open(void) { #endif #endif +#define BLUEPY_URL "https://github.com/IanHarvey/bluepy" +extern const char* bluepy_helper_version; /* defined in version.c */ + static GIOChannel *iochannel = NULL; static GAttrib *attrib = NULL; static GMainLoop *event_loop; @@ -154,8 +159,11 @@ static const char *st_CONNECTED = "conn", *st_SCANNING = "scan"; -// delimits fields in response message -#define RESP_DELIM "\x1e" +/* field delimiter response messages. + Using RECORD SEPARATOR (ASCII \x1e) as delimiter makes it easier to + accomodate strings with spaces without escaping them. + Using SPACE (ascii \x20) is easier for manual debugging. */ +static gchar resp_delim = '\x1e'; static void resp_begin(const char *rsptype) { @@ -164,22 +172,26 @@ static void resp_begin(const char *rsptype) static void send_sym(const char *tag, const char *val) { - printf(RESP_DELIM "%s=$%s", tag, val); + putchar(resp_delim); + printf("%s=$%s", tag, val); } static void send_uint(const char *tag, unsigned int val) { - printf(RESP_DELIM "%s=h%X", tag, val); + putchar(resp_delim); + printf("%s=h%X", tag, val); } static void send_str(const char *tag, const char *val) { - printf(RESP_DELIM "%s='%s", tag, val); + putchar(resp_delim); + printf("%s='%s", tag, val); } static void send_data(const unsigned char *val, size_t len) { - printf(RESP_DELIM "%s=b", tag_DATA); + putchar(resp_delim); + printf("%s=b", tag_DATA); while ( len-- > 0 ) printf("%02X", *val++); } @@ -187,7 +199,8 @@ static void send_data(const unsigned char *val, size_t len) static void send_addr(const struct mgmt_addr_info *addr) { const uint8_t *val = addr->bdaddr.b; - printf(RESP_DELIM "%s=b", tag_ADDR); + putchar(resp_delim); + printf("%s=b", tag_ADDR); int len = 6; /* Human-readable byte order is reverse of bdaddr.b */ while ( len-- > 0 ) @@ -1722,7 +1735,10 @@ static void parse_line(char *line_read) if (*line_read == '\0') goto done; - g_shell_parse_argv(line_read, &argcp, &argvp, NULL); + if (!g_shell_parse_argv(line_read, &argcp, &argvp, NULL)) { + resp_error(err_BAD_CMD); + goto done; + } for (i = 0; commands[i].cmd; i++) if (strcasecmp(commands[i].cmd, argvp[0]) == 0) @@ -1860,10 +1876,40 @@ static void mgmt_setup(unsigned int idx) } } +static void show_help(const char* progname) +{ + printf("usage: %s [-hvs] [index]\n", progname); + puts("\n\ +bluepy-helper is a BlueZ-based bluetooth low-level interface.\n\ +It is used by the Bluepy python package.\n\ +See: " BLUEPY_URL "\n\ +\n\ +Options:\n\ + index - an integer specifing the bluetooth hci interface\n\ + number (0 = hci0). Optional if connecting to an already-paired\n\ + device, but required for explicit pair/unpair commands.\n\ +\n\ + -h Show this help screen.\n\ + -v Show version and exit.\n\ + -s Use SPACE character as field delimiter (instead of the default\n\ + FIELD SEPARATOR (ascii \\x1E). Useful for manual debugging.\n\ +"); + + exit(EXIT_SUCCESS); +} + +static void show_version() +{ + printf("bluepy-helper version %s\n", bluepy_helper_version); + puts("See: " BLUEPY_URL); + exit(EXIT_SUCCESS); +} + int main(int argc, char *argv[]) { GIOChannel *pchan; gint events; + int opt; opt_sec_level = g_strdup("low"); @@ -1873,11 +1919,32 @@ int main(int argc, char *argv[]) DBG(__FILE__ " built at " __TIME__ " on " __DATE__); - if (argc > 1) { + + while ((opt = getopt(argc, argv, "shv")) != -1) { + switch (opt) + { + case 'h': + show_help(argv[0]); /* does not return */ + + case 'v': + show_version(); /* does not return */ + + case 's': + /* use SPACE for field delimiter in response strings */ + resp_delim = ' '; + break; + + default: /* '?' */ + errx(EXIT_FAILURE, "Use -h for help"); + } + } + + if (argc > optind) { int index; - if (sscanf (argv[1], "%i", &index)!=1) { - DBG("error converting argument: %s to device index integer",argv[1]); + if (sscanf (argv[optind], "%i", &index)!=1) { + warnx("invalid device index '%s'", argv[optind]); + DBG("error converting argument: %s to device index integer",argv[optind]); } else { mgmt_setup(index); } diff --git a/bluepy/btle.py b/bluepy/btle.py index ed4939b4..e634be15 100755 --- a/bluepy/btle.py +++ b/bluepy/btle.py @@ -44,12 +44,44 @@ class BTLEException(Exception): GATT_ERROR = 4 MGMT_ERROR = 5 - def __init__(self, code, message): + ERROR_STR = { + DISCONNECTED : "Disconnect Error", + COMM_ERROR : "Communication Error", + INTERNAL_ERROR : "Internal Error", + GATT_ERROR : "GATT Error", + MGMT_ERROR : "MGMT Error" + } + + def __init__(self, code, message, resp_dict=None): self.code = code self.message = message + # optional messages from bluepy-helper + self.estat = None + self.emsg = None + if resp_dict: + self.estat = resp_dict.get('estat',None) + if isinstance(self.estat,list): + self.estat = self.estat[0] + self.emsg = resp_dict.get('emsg',None) + if isinstance(self.emsg,list): + self.emsg = self.emsg[0] + + def __str__(self): - return self.message + msg = self.ERROR_STR.get(self.code,"UNKNOWN Error (code: %s)" % (self.code)) + msg = msg + ": " + self.message + if self.estat or self.emsg: + msg = msg + " (" + if self.estat: + msg = msg + "estat: %s" % self.estat + if self.estat and self.emsg: + msg = msg + " " + if self.emsg: + msg = msg + "emsg: %s" % self.emsg + msg = msg + ")" + + return msg class UUID: @@ -280,7 +312,7 @@ def _mgmtCmd(self, cmd): if rsp['code'][0] != 'success': self._stopHelper() raise BTLEException(BTLEException.DISCONNECTED, - "Failed to execute mgmt cmd '%s'" % (cmd)) + "Failed to execute mgmt cmd '%s'" % (cmd), rsp) @staticmethod def parseResp(line): @@ -331,13 +363,13 @@ def _waitResp(self, wantType, timeout=None): elif respType == 'stat': if 'state' in resp and len(resp['state']) > 0 and resp['state'][0] == 'disc': self._stopHelper() - raise BTLEException(BTLEException.DISCONNECTED, "Device disconnected") + raise BTLEException(BTLEException.DISCONNECTED, "Device disconnected", resp) elif respType == 'err': errcode=resp['code'][0] if errcode=='nomgmt': - raise BTLEException(BTLEException.MGMT_ERROR, "Management not available (permissions problem?)") + raise BTLEException(BTLEException.MGMT_ERROR, "Management not available (permissions problem?)", resp) else: - raise BTLEException(BTLEException.COMM_ERROR, "Error from Bluetooth stack (%s)" % errcode) + raise BTLEException(BTLEException.COMM_ERROR, "Error from Bluetooth stack (%s)" % errcode, resp) elif respType == 'scan': # Scan response when we weren't interested. Ignore it continue @@ -407,7 +439,7 @@ def _connect(self, addr, addrType=ADDR_TYPE_PUBLIC, iface=None): if rsp['state'][0] != 'conn': self._stopHelper() raise BTLEException(BTLEException.DISCONNECTED, - "Failed to connect to peripheral %s, addr type: %s" % (addr, addrType)) + "Failed to connect to peripheral %s, addr type: %s" % (addr, addrType), rsp) def connect(self, addr, addrType=ADDR_TYPE_PUBLIC, iface=None): if isinstance(addr, ScanEntry): @@ -454,7 +486,7 @@ def getServiceByUUID(self, uuidVal): self._writeCmd("svcs %s\n" % uuid) rsp = self._getResp('find') if 'hstart' not in rsp: - raise BTLEException(BTLEException.GATT_ERROR, "Service %s not found" % (uuid.getCommonName())) + raise BTLEException(BTLEException.GATT_ERROR, "Service %s not found" % (uuid.getCommonName()), rsp) svc = Service(self, uuid, rsp['hstart'][0], rsp['hend'][0]) if self._serviceMap is None: @@ -513,8 +545,11 @@ def setSecurityLevel(self, level): self._writeCmd("secu %s\n" % level) return self._getResp('stat') - def unpair(self, address): - self._mgmtCmd("unpair %s" % (address)) + def pair(self): + self._mgmtCmd("pair") + + def unpair(self): + self._mgmtCmd("unpair") def setMTU(self, mtu): self._writeCmd("mtu %x\n" % mtu)