Repository: njh/mqtt-sn-tools Branch: main Commit: a1dd870684f6 Files: 24 Total size: 140.0 KB Directory structure: gitextract_cmeey4ae/ ├── .astylerc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── NEWS.md ├── README.md ├── mqtt-sn-dump.c ├── mqtt-sn-pub.c ├── mqtt-sn-serial-bridge.c ├── mqtt-sn-sub.c ├── mqtt-sn.c ├── mqtt-sn.h └── test/ ├── .ruby-version ├── Gemfile ├── Rakefile ├── fake_server.rb ├── mqtt-sn-dump-test.rb ├── mqtt-sn-pub-test.rb ├── mqtt-sn-serial-bridge-test.rb ├── mqtt-sn-sub-test.rb ├── test.txt ├── test_big.txt └── test_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .astylerc ================================================ # use the K&R code style --style=kr # Indent 4 spaces --indent=spaces=4 # Indent switch and case statements --indent-switches # We don't need .orig files, because we are version controlled --suffix=none ================================================ FILE: .gitignore ================================================ mqtt-sn-dump mqtt-sn-pub mqtt-sn-sub mqtt-sn-serial-bridge *.exe *.o *.tar.gz # Code coverage files *.gcda *.gcno coverage/* # Eclipse project metafiles .settings .cproject .project ================================================ FILE: .travis.yml ================================================ # We use ruby for running the tests language: ruby sudo: false before_install: - gem update bundler rvm: - '2.5.5' script: - make - make test ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) ===================== Copyright (c) Nicholas J Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ CC=cc PACKAGE=mqtt-sn-tools VERSION=0.0.7 CFLAGS=-g -Wall -DVERSION=$(VERSION) LDFLAGS= INSTALL?=install prefix=/usr/local TARGETS=mqtt-sn-dump mqtt-sn-pub mqtt-sn-sub mqtt-sn-serial-bridge .PHONY : all install uninstall clean dist test coverage all: $(TARGETS) $(TARGETS): %: mqtt-sn.o %.o $(CC) $(LDFLAGS) -o $@ $^ %.o : %.c mqtt-sn.h $(CC) $(CFLAGS) -c $< install: $(TARGETS) $(INSTALL) -d "$(DESTDIR)$(prefix)/bin" $(INSTALL) -s $(TARGETS) "$(DESTDIR)$(prefix)/bin" uninstall: @for target in $(TARGETS); do \ cmd="rm -f $(DESTDIR)$(prefix)/bin/$$target"; \ echo "$$cmd" && $$cmd; \ done clean: -rm -f *.o *.gcda *.gcno $(TARGETS) -rm -Rf coverage dist: distdir='$(PACKAGE)-$(VERSION)'; mkdir $$distdir || exit 1; \ list=`git ls-files`; for file in $$list; do \ cp -pR $$file $$distdir || exit 1; \ done; \ tar -zcf $$distdir.tar.gz $$distdir; \ rm -fr $$distdir test: all @(which bundle > /dev/null) || (echo "Ruby Bundler is not installed"; exit -1) cd test && bundle install && bundle exec rake test # Use gcc for coverage report - it works better than clang/llvm coverage: CC=gcc coverage: CFLAGS += --coverage coverage: LDFLAGS += --coverage coverage: clean test mkdir -p coverage lcov \ --capture \ --directory . \ --no-external \ --output coverage/lcov.info genhtml \ --title 'MQTT-SN Tools' \ --output-directory coverage \ coverage/lcov.info ================================================ FILE: NEWS.md ================================================ Release History =============== Version 0.0.6 (2017-08-23) -------------------------- - Added support for publishing and subscribing with QoS 1 - Added support for reading from a file using -f - Added support for reading from STDIN using -s - Added support for publishing one message per line using -l - Added support for subscribing to multiple topics - Added timeout when waiting for packets - Made Keep Alive configurable in mqtt-sn-pub - Changed default keep alive for mqtt-sn-pub to 10 seconds - Fix for displaying IPv6 addresses - Improved debug error messages when connecting fails Version 0.0.5 (2015-11-04) -------------------------- - Added extensive test suite written in Ruby - Added return code strings to packet dump output - Display when connecting and disconnecting starts. - Changed error messages that aren't fatal to warnings - Added signal handling to mqtt-sn-dump - Fix to ensure that packet data structures are set to 0 before use - Added mqtt-sn-dump tool - Added debug of source IP and port when receiving a packet - Implemented setting the Clean Session flag - Fix for subscribing to short/predefined topics - New command line option -V to display current time and the topic name in verbose mode. - Added install and uninstall targets to the Makefile - Added Forwarder Encapsulation support - Added current time and log level to log messages - Wait for disconnect acknowledge into mqtt-sn-pub and mqtt-sn-sub. - Display string, not hex for unexpected packet type - Fixed incorrect pointer handling in mqtt-sn-sub Version 0.0.4 (2014-06-01) -------------------------- - Changed license to MIT - Added mqtt-sn-serial-bridge - Simplified the Makefile - Improvements to cope with multiple packets received in close succession Version 0.0.3 (2013-12-07) -------------------------- - Added BSD 2-Clause License - Renamed 'Unsupported Features' to limitations. - Added checks to make sure various strings aren’t too long - Corrected the maximum length of a MQTT-SN packet (in this tool) - Added support for displaying short and pre-defined topic types in verbose mode - Added support for pre-defined Topic IDs in the subscribing client (note that this is untested because I don’t have a broker that supports it) - Added support for displaying topic name for wildcard subscriptions (aka verbose mode) - Noted that QoS −1 is now supported in the README - Added support for QoS Level −1 (tested against RSMB) - Corrected help text for pre-defined topic ID parameter - Shortened clientid argument help text - Enabled keep-alive timer in mqtt-sn-pub - will cause sessions to be cleaned up by the broker if client is unexpectedly disconnected Version 0.0.2 (2013-12-02) -------------------------- - Renamed from MQTT-S to MQTT-SN to match specification change - Added support for publishing to short and pre-defined topic ids - Added setting of QoS flag in matts_send_publish - Renamed default client id prefix to mqtts-tools- Version 0.0.1 (2013-04-02) -------------------------- - Initial Release - Contains just mqtts-sub and mqtts-pub ================================================ FILE: README.md ================================================ MQTT-SN Tools ============= [![Build Status](https://travis-ci.org/njh/mqtt-sn-tools.svg?branch=master)](https://travis-ci.org/njh/mqtt-sn-tools) Command line tools written in C for the MQTT-SN (MQTT for Sensor Networks) protocol. Supported Features ------------------ - QoS 0, 1 and -1 - Keep alive pings - Publishing retained messages - Publishing empty messages - Subscribing to named topic - Clean / unclean sessions - Manual and automatic client ID generation - Displaying topic name with wildcard subscriptions - Pre-defined topic IDs and short topic names - Forwarder encapsulation according to MQTT-SN Protocol Specification v1.2. Limitations ----------- - Packets must be 255 or less bytes long - No Last Will and Testament - No QoS 2 - No automatic re-sending of lost packets - No Automatic gateway discovery Building -------- Just run 'make' on a POSIX system. Publishing ---------- Usage: mqtt-sn-pub [opts] -t -m -d Increase debug level by one. -d can occur multiple times. -f A file to send as the message payload. -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id. -k keep alive in seconds for this client. Defaults to 10. -e sleep duration in seconds when disconnecting. Defaults to 0. -m Message payload to send. -l Read from STDIN, one message per line. -n Send a null (zero length) message. -p Network port to connect to. Defaults to 1883. -q Quality of Service value (0, 1 or -1). Defaults to 0. -r Message should be retained. -s Read one whole message from STDIN. -t MQTT-SN topic name to publish to. -T Pre-defined MQTT-SN topic ID to publish to. --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id. --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. Subscribing ----------- Usage: mqtt-sn-sub [opts] -t -1 exit after receiving a single message. -c disable 'clean session' (store subscription and pending messages when client disconnects). -d Increase debug level by one. -d can occur multiple times. -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id. -k keep alive in seconds for this client. Defaults to 10. -e sleep duration in seconds when disconnecting. Defaults to 0. -p Network port to connect to. Defaults to 1883. -q QoS level to subscribe with (0 or 1). Defaults to 0. -t MQTT-SN topic name to subscribe to. It may repeat multiple times. -T Pre-defined MQTT-SN topic ID to subscribe to. It may repeat multiple times. --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id. -v Print messages verbosely, showing the topic name. -V Print messages verbosely, showing current time and the topic name. --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. Dumping ------- Displays MQTT-SN packets sent to specified port. Most useful for listening out for QoS -1 messages being published by a client. Usage: mqtt-sn-dump [opts] -p -a Dump all packet types. -d Increase debug level by one. -d can occur multiple times. -p Network port to listen on. Defaults to 1883. -v Print messages verbosely, showing the topic name. Serial Port Bridge ------------------ The Serial Port bridge can be used to relay packets from a remote device on the end of a serial port and convert them into UDP packets, which are sent and received from a broker or MQTT-SN gateway. Usage: mqtt-sn-serial-bridge [opts] -b Set the baud rate. Defaults to 9600. -d Increase debug level by one. -d can occur multiple times. -dd Enable extended debugging - display packets in hex. -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. -p Network port to connect to. Defaults to 1883. --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. License ------- MQTT-SN Tools is licensed under the [MIT License]. [MIT License]: http://opensource.org/licenses/MIT ================================================ FILE: mqtt-sn-dump.c ================================================ /* MQTT-SN command-line dump Copyright (C) Nicholas Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include "mqtt-sn.h" const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; uint8_t dump_all = FALSE; uint8_t debug = 0; uint8_t verbose = 0; uint8_t keep_running = TRUE; static void usage() { fprintf(stderr, "Usage: mqtt-sn-dump [opts] -p \n"); fprintf(stderr, "\n"); fprintf(stderr, " -a Dump all packet types.\n"); fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); fprintf(stderr, " -p Network port to listen on. Defaults to %s.\n", mqtt_sn_port); fprintf(stderr, " -v Print messages verbosely, showing the topic name.\n"); exit(EXIT_FAILURE); } static void parse_opts(int argc, char** argv) { int ch; // Parse the options/switches while((ch = getopt(argc, argv, "adp:v?")) != -1) switch(ch) { case 'a': dump_all = TRUE; break; case 'd': debug++; break; case 'p': mqtt_sn_port = optarg; break; case 'v': verbose++; break; case '?': default: usage(); break; } } static int bind_udp_socket(const char* port_str) { struct sockaddr_in si_me; short port = atoi(port_str); int sock; if ((sock=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) { perror("socket"); exit(EXIT_FAILURE); } memset((char *) &si_me, 0, sizeof(si_me)); si_me.sin_family = AF_INET; si_me.sin_port = htons(port); si_me.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (const struct sockaddr *)&si_me, sizeof(si_me)) == -1) { perror("bind"); exit(EXIT_FAILURE); } mqtt_sn_log_debug("mqtt-sn-dump listening on port %s", port_str); return sock; } static void termination_handler (int signum) { switch(signum) { case SIGHUP: mqtt_sn_log_debug("Got hangup signal."); break; case SIGTERM: mqtt_sn_log_debug("Got termination signal."); break; case SIGINT: mqtt_sn_log_debug("Got interrupt signal."); break; } // Signal the main thread to stop keep_running = FALSE; } int main(int argc, char* argv[]) { int sock; // Disable buffering on STDOUT setvbuf(stdout, NULL, _IONBF, 0); // Parse the command-line options parse_opts(argc, argv); // Enable debugging? mqtt_sn_set_debug(debug); mqtt_sn_set_verbose(verbose); // Setup signal handlers signal(SIGTERM, termination_handler); signal(SIGINT, termination_handler); signal(SIGHUP, termination_handler); // Create a listening UDP socket sock = bind_udp_socket(mqtt_sn_port); while (keep_running) { int ret = mqtt_sn_select(sock); if (ret < 0) { break; } else if (ret > 0) { char* packet = mqtt_sn_receive_packet(sock); if (dump_all) { mqtt_sn_dump_packet(packet); } else if (packet[1] == MQTT_SN_TYPE_PUBLISH) { mqtt_sn_print_publish_packet((publish_packet_t *)packet); } } } close(sock); return 0; } ================================================ FILE: mqtt-sn-pub.c ================================================ /* MQTT-SN command-line publishing client Copyright (C) Nicholas Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include "mqtt-sn.h" const char *client_id = NULL; const char *topic_name = NULL; const char *message_data = NULL; const char *message_file = NULL; const char *mqtt_sn_host = "127.0.0.1"; const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; uint16_t source_port = 0; uint16_t topic_id = 0; uint8_t topic_id_type = MQTT_SN_TOPIC_TYPE_NORMAL; uint16_t keep_alive = MQTT_SN_DEFAULT_KEEP_ALIVE; uint16_t sleep_duration = 0; int8_t qos = 0; uint8_t retain = FALSE; uint8_t one_message_per_line = FALSE; uint8_t debug = 0; static void usage() { fprintf(stderr, "Usage: mqtt-sn-pub [opts] -t -m \n"); fprintf(stderr, "\n"); fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); fprintf(stderr, " -f A file to send as the message payload.\n"); fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); fprintf(stderr, " -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id.\n"); fprintf(stderr, " -k keep alive in seconds for this client. Defaults to %d.\n", keep_alive); fprintf(stderr, " -e sleep duration in seconds when disconnecting. Defaults to %d.\n", sleep_duration); fprintf(stderr, " -m Message payload to send.\n"); fprintf(stderr, " -l Read from STDIN, one message per line.\n"); fprintf(stderr, " -n Send a null (zero length) message.\n"); fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); fprintf(stderr, " -q Quality of Service value (0, 1 or -1). Defaults to %d.\n", qos); fprintf(stderr, " -r Message should be retained.\n"); fprintf(stderr, " -s Read one whole message from STDIN.\n"); fprintf(stderr, " -t MQTT-SN topic name to publish to.\n"); fprintf(stderr, " -T Pre-defined MQTT-SN topic ID to publish to.\n"); fprintf(stderr, " --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation.\n"); fprintf(stderr, " --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id.\n"); fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); exit(EXIT_FAILURE); } static void parse_opts(int argc, char** argv) { static struct option long_options[] = { {"fe", no_argument, 0, 1000 }, {"wlnid", required_argument, 0, 1001 }, {"cport", required_argument, 0, 1002 }, {0, 0, 0, 0} }; int ch; /* getopt_long stores the option index here. */ int option_index = 0; // Parse the options/switches while ((ch = getopt_long (argc, argv, "df:h:i:k:e:lm:np:q:rst:T:?", long_options, &option_index)) != -1) { switch (ch) { case 'd': debug++; break; case 'h': mqtt_sn_host = optarg; break; case 'f': message_file = optarg; break; case 'i': client_id = optarg; break; case 'l': message_file = "-"; one_message_per_line = TRUE; break; case 'm': message_data = optarg; break; case 'k': keep_alive = atoi(optarg); break; case 'e': sleep_duration = atoi(optarg); break; case 'n': message_data = ""; break; case 'p': mqtt_sn_port = optarg; break; case 'q': qos = atoi(optarg); break; case 'r': retain = TRUE; break; case 's': message_file = "-"; one_message_per_line = FALSE; break; case 't': topic_name = optarg; break; case 'T': topic_id = atoi(optarg); break; case 1000: mqtt_sn_enable_frwdencap(); break; case 1001: mqtt_sn_set_frwdencap_parameters((uint8_t*)optarg, strlen(optarg)); break; case 1002: source_port = atoi(optarg); break; case '?': default: usage(); break; } // switch } // while // Missing Parameter? if (!(topic_name || topic_id) || !(message_data || message_file)) { usage(); } if (qos != -1 && qos != 0 && qos != 1) { mqtt_sn_log_err("Only QoS level 0, 1 or -1 is supported."); exit(EXIT_FAILURE); } // Both topic name and topic id? if (topic_name && topic_id) { mqtt_sn_log_err("Please provide either a topic id or a topic name, not both."); exit(EXIT_FAILURE); } // Both message data and file? if (message_data && message_file) { mqtt_sn_log_err("Please provide either message data or a message file, not both."); exit(EXIT_FAILURE); } // Check topic is valid for QoS level -1 if (qos == -1 && topic_id == 0 && strlen(topic_name) != 2) { mqtt_sn_log_err("Either a pre-defined topic id or a short topic name must be given for QoS -1."); exit(EXIT_FAILURE); } } static void publish_file(int sock, const char* filename) { char buffer[MQTT_SN_MAX_PAYLOAD_LENGTH]; uint16_t message_len = 0; FILE* file = NULL; if (strcmp(filename, "-") == 0) { file = stdin; } else { file = fopen(filename, "rb"); } if (!file) { perror("Failed to open message file"); exit(EXIT_FAILURE); } if (one_message_per_line) { // One message per line while(!feof(file) && !ferror(file)) { char* message = fgets(buffer, MQTT_SN_MAX_PAYLOAD_LENGTH, file); if (message) { char* end = strpbrk(message, "\n\r"); if (end) { uint16_t message_len = (end - message); mqtt_sn_send_publish(sock, topic_id, topic_id_type, message, message_len, qos, retain); } else { mqtt_sn_log_err("Failed to find newline when reading message"); } } else if (ferror(file)) { perror("Failed to read message line"); exit(EXIT_FAILURE); } } } else { // One message until EOF message_len = fread(buffer, 1, MQTT_SN_MAX_PAYLOAD_LENGTH, file); if (ferror(file)) { perror("Failed to read message file"); exit(EXIT_FAILURE); } else if (!feof(file)) { mqtt_sn_log_warn("Input file is longer than the maximum message size"); } mqtt_sn_send_publish(sock, topic_id, topic_id_type, buffer, message_len, qos, retain); } fclose(file); } int main(int argc, char* argv[]) { int sock; mqtt_sn_disable_frwdencap(); // Parse the command-line options parse_opts(argc, argv); // Enable debugging? mqtt_sn_set_debug(debug); mqtt_sn_set_timeout(keep_alive / 2); // Create a UDP socket sock = mqtt_sn_create_socket(mqtt_sn_host, mqtt_sn_port, source_port); if (sock) { // Connect to gateway if (qos >= 0) { mqtt_sn_log_debug("Connecting..."); mqtt_sn_send_connect(sock, client_id, keep_alive, TRUE); mqtt_sn_receive_connack(sock); } if (topic_id) { // Use pre-defined topic ID topic_id_type = MQTT_SN_TOPIC_TYPE_PREDEFINED; } else if (strlen(topic_name) == 2) { // Convert the 2 character topic name into a 2 byte topic id topic_id = (topic_name[0] << 8) + topic_name[1]; topic_id_type = MQTT_SN_TOPIC_TYPE_SHORT; } else if (qos >= 0) { // Register the topic name mqtt_sn_send_register(sock, topic_name); topic_id = mqtt_sn_receive_regack(sock); topic_id_type = MQTT_SN_TOPIC_TYPE_NORMAL; } // Publish to the topic if (message_file) { publish_file(sock, message_file); } else { uint16_t message_len = strlen(message_data); mqtt_sn_send_publish(sock, topic_id, topic_id_type, message_data, message_len, qos, retain); } // Finally, disconnect if (qos >= 0) { mqtt_sn_log_debug("Disconnecting..."); mqtt_sn_send_disconnect(sock, sleep_duration); mqtt_sn_receive_disconnect(sock); } close(sock); } mqtt_sn_cleanup(); return 0; } ================================================ FILE: mqtt-sn-serial-bridge.c ================================================ /* MQTT-SN serial bridge Copyright (C) Nicholas Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //#define _POSIX_SOURCE 1 /* POSIX compliant source */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mqtt-sn.h" const char *mqtt_sn_host = "127.0.0.1"; const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; const char *serial_device = NULL; uint16_t source_port = 0; int serial_baud = 9600; uint8_t debug = 0; uint8_t frwdencap = FALSE; uint8_t keep_running = TRUE; static speed_t baud_lookup(int baud) { switch(baud) { case 0: return B0; case 50: return B50; case 75: return B75; case 110: return B110; case 134: return B134; case 150: return B150; case 200: return B200; case 300: return B300; case 600: return B600; case 1200: return B1200; case 1800: return B1800; case 2400: return B2400; case 4800: return B4800; case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; default: fprintf(stderr, "Unsupported baud rate: %d\n", baud); exit(1); } } static void usage() { fprintf(stderr, "Usage: mqtt-sn-serial-bridge [opts] \n"); fprintf(stderr, "\n"); fprintf(stderr, " -b Set the baud rate. Defaults to %d.\n", (int)serial_baud); fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); fprintf(stderr, " -dd Enable extended debugging - display packets in hex.\n"); fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); fprintf(stderr, " --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation.\n"); fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); exit(EXIT_FAILURE); } static void parse_opts(int argc, char** argv) { static struct option long_options[] = { {"fe", no_argument, 0, 1000 }, {"cport", required_argument, 0, 1001 }, {0, 0, 0, 0} }; int ch; /* getopt_long stores the option index here. */ int option_index = 0; // Parse the options/switches while ((ch = getopt_long (argc, argv, "b:dh:p:?", long_options, &option_index)) != -1) { switch (ch) { case 'b': serial_baud = atoi(optarg); baud_lookup(serial_baud); break; case 'd': debug++; break; case 'h': mqtt_sn_host = optarg; break; case 'p': mqtt_sn_port = optarg; break; case 1000: mqtt_sn_enable_frwdencap(); frwdencap = TRUE; break; case 1001: source_port = atoi(optarg); break; case '?': default: usage(); break; } // switch } // while // Final argument is the serial port device path if (argc-optind < 1) { fprintf(stderr, "Missing serial port.\n"); usage(); } else { serial_device = argv[optind]; } } static int serial_open(const char* device_path) { struct termios tios; int fd; mqtt_sn_disable_frwdencap(); mqtt_sn_log_debug("Opening %s", device_path); fd = open(device_path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0) { perror(device_path); exit(EXIT_FAILURE); } // Turn back on blocking reads fcntl(fd, F_SETFL, 0); // Read existing serial port settings tcgetattr(fd, &tios); // Set the input and output baud rates cfsetispeed(&tios, baud_lookup(serial_baud)); cfsetospeed(&tios, baud_lookup(serial_baud)); // Set to local mode tios.c_cflag |= CLOCAL | CREAD; // Set serial port mode to 8N1 tios.c_cflag &= ~PARENB; tios.c_cflag &= ~CSTOPB; tios.c_cflag &= ~CSIZE; tios.c_cflag |= CS8; // Turn off flow control and ignore parity tios.c_iflag &= ~(IXON | IXOFF | IXANY); tios.c_iflag |= IGNPAR; // Turn off output post-processing tios.c_oflag &= ~OPOST; // set input mode (non-canonical, no echo,...) tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // Set VMIN high, so that we wait for a gap in the data // http://www.unixwiz.net/techtips/termios-vmin-vtime.html tios.c_cc[VMIN] = 255; tios.c_cc[VTIME] = 1; tcsetattr(fd, TCSAFLUSH, &tios); // Flush the input buffer sleep(1); tcflush(fd, TCIOFLUSH); return fd; } static void* serial_read_packet(int fd) { static uint8_t buf[MQTT_SN_MAX_PACKET_LENGTH+1]; // First get the length of the packet int bytes_read = read(fd, buf, 1); if (bytes_read != 1) { mqtt_sn_log_err("Error reading packet length from serial port: %d, %d", bytes_read, errno); return NULL; } if (buf[0] == 0x00) { mqtt_sn_log_err("Packets of length 0 are invalid."); return NULL; } // Read in the rest of the packet bytes_read = read(fd, &buf[1], buf[0]-1); if (bytes_read <= 0) { mqtt_sn_log_err("Error reading rest of packet from serial port: %d, %d", bytes_read, errno); return NULL; } else { bytes_read += 1; } if (mqtt_sn_validate_packet(buf, bytes_read) == FALSE) { return NULL; } // NULL-terminate the packet buf[bytes_read] = '\0'; if (debug) { const char* type = mqtt_sn_type_string(buf[1]); mqtt_sn_log_debug("Serial -> UDP (bytes_read=%d, type=%s)", bytes_read, type); if (debug > 1) { int i; fprintf(stderr, " "); for (i=0; i #include #include #include #include #include #include #include #include #include #include #include #include "mqtt-sn.h" const char *client_id = NULL; // Array of pointers to topic names char **topic_name_ar = NULL; // Topic names array size. When too small it is incremented by ARRAY_INCREMENT. uint16_t topic_name_ar_size = 0; uint16_t topic_name_index = 0; // Array of predefined topic IDs to subscribe to uint16_t *predef_topic_id_ar = NULL; // Predefined topic IDs array size. When too small it is incremented by ARRAY_INCREMENT. uint16_t predef_topic_id_ar_size = 0; uint16_t predef_topic_id_index = 0; #define ARRAY_INCREMENT 10 const char *mqtt_sn_host = "127.0.0.1"; const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; uint16_t source_port = 0; uint16_t keep_alive = MQTT_SN_DEFAULT_KEEP_ALIVE; uint16_t sleep_duration = 0; int8_t qos = 0; uint8_t retain = FALSE; uint8_t debug = 0; uint8_t single_message = FALSE; uint8_t clean_session = TRUE; uint8_t verbose = 0; uint8_t keep_running = TRUE; static void usage() { fprintf(stderr, "Usage: mqtt-sn-sub [opts] -t \n"); fprintf(stderr, "\n"); fprintf(stderr, " -1 exit after receiving a single message.\n"); fprintf(stderr, " -c disable 'clean session' (store subscription and pending messages when client disconnects).\n"); fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); fprintf(stderr, " -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id.\n"); fprintf(stderr, " -k keep alive in seconds for this client. Defaults to %d.\n", keep_alive); fprintf(stderr, " -e sleep duration in seconds when disconnecting. Defaults to %d.\n", sleep_duration); fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); fprintf(stderr, " -q QoS level to subscribe with (0 or 1). Defaults to %d.\n", qos); fprintf(stderr, " -t MQTT-SN topic name to subscribe to. It may repeat multiple times.\n"); fprintf(stderr, " -T Pre-defined MQTT-SN topic ID to subscribe to. It may repeat multiple times.\n"); fprintf(stderr, " --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation.\n"); fprintf(stderr, " --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id.\n"); fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); fprintf(stderr, " -v Print messages verbosely, showing the topic name.\n"); fprintf(stderr, " -V Print messages verbosely, showing current time and the topic name.\n"); exit(EXIT_FAILURE); } static void parse_opts(int argc, char** argv) { static struct option long_options[] = { {"fe", no_argument, 0, 1000 }, {"wlnid", required_argument, 0, 1001 }, {"cport", required_argument, 0, 1002 }, {0, 0, 0, 0} }; int ch; /* getopt_long stores the option index here. */ int option_index = 0; // Parse the options/switches while ((ch = getopt_long(argc, argv, "1cdh:i:k:e:p:q:t:T:vV?", long_options, &option_index)) != -1) switch (ch) { case '1': single_message = TRUE; break; case 'c': clean_session = FALSE; break; case 'd': debug++; break; case 'h': mqtt_sn_host = optarg; break; case 'i': client_id = optarg; break; case 'k': keep_alive = atoi(optarg); break; case 'e': sleep_duration = atoi(optarg); break; case 'p': mqtt_sn_port = optarg; break; case 'q': qos = atoi(optarg); break; case 't': // Resize topic names array when too small if (topic_name_index == topic_name_ar_size) { topic_name_ar = realloc(topic_name_ar, (topic_name_ar_size + ARRAY_INCREMENT) * sizeof(char*)) ; topic_name_ar_size += ARRAY_INCREMENT; } topic_name_ar[topic_name_index++] = optarg; break; case 'T': // Resize predefined topic IDs array when too small if (predef_topic_id_index == predef_topic_id_ar_size) { predef_topic_id_ar = realloc(predef_topic_id_ar, (predef_topic_id_ar_size + ARRAY_INCREMENT) * sizeof(uint16_t)); predef_topic_id_ar_size += ARRAY_INCREMENT; } predef_topic_id_ar[predef_topic_id_index++] = atoi(optarg); break; case 1000: mqtt_sn_enable_frwdencap(); break; case 1001: mqtt_sn_set_frwdencap_parameters((uint8_t*)optarg, strlen(optarg)); break; case 1002: source_port = atoi(optarg); break; case 'v': // Prevent -v setting verbose level back down to 1 if already set to 2 by -V verbose = (verbose == 0) ? 1 : verbose; break; case 'V': verbose = 2; break; case '?': default: usage(); break; } // Missing Parameter? if (!topic_name_index && !predef_topic_id_index) { usage(); } } static void termination_handler (int signum) { switch(signum) { case SIGHUP: mqtt_sn_log_debug("Got hangup signal."); break; case SIGTERM: mqtt_sn_log_debug("Got termination signal."); break; case SIGINT: mqtt_sn_log_debug("Got interrupt signal."); break; } // Signal the main thread to stop keep_running = FALSE; } int main(int argc, char* argv[]) { int sock; mqtt_sn_disable_frwdencap(); // Parse the command-line options parse_opts(argc, argv); // Enable debugging? mqtt_sn_set_debug(debug); mqtt_sn_set_verbose(verbose); mqtt_sn_set_timeout(keep_alive / 2); // Setup signal handlers signal(SIGTERM, termination_handler); signal(SIGINT, termination_handler); signal(SIGHUP, termination_handler); // Create a UDP socket sock = mqtt_sn_create_socket(mqtt_sn_host, mqtt_sn_port, source_port); if (sock) { // Connect to server mqtt_sn_log_debug("Connecting..."); mqtt_sn_send_connect(sock, client_id, keep_alive, clean_session); mqtt_sn_receive_connack(sock); uint16_t i; // Subscribe to the each topic name for (i = 0; i < topic_name_index; i++) { mqtt_sn_log_debug("Subscribing to topic name: %s ...", topic_name_ar[i]); mqtt_sn_send_subscribe_topic_name(sock, topic_name_ar[i], qos); // Wait for the subscription acknowledgment uint16_t topic_id = mqtt_sn_receive_suback(sock); if (topic_id && strlen(topic_name_ar[i]) > 2) { mqtt_sn_register_topic(topic_id, topic_name_ar[i]); } } // Subscribe to the each predefined topic ID for (i = 0; i < predef_topic_id_index; i++) { mqtt_sn_log_debug("Subscribing to predefined topic ID: %u ...", predef_topic_id_ar[i]); mqtt_sn_send_subscribe_topic_id(sock, predef_topic_id_ar[i], qos); // Wait for the subscription acknowledgment mqtt_sn_receive_suback(sock); } // Keep processing packets until process is terminated while(keep_running) { publish_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_PUBLISH, sock); if (packet) { uint8_t packet_qos = packet->flags & MQTT_SN_FLAG_QOS_MASK; if (packet_qos == MQTT_SN_FLAG_QOS_1) { mqtt_sn_send_puback(sock, packet, MQTT_SN_ACCEPTED); } if (single_message) { break; } } } // Finally, disconnect mqtt_sn_log_debug("Disconnecting..."); mqtt_sn_send_disconnect(sock, sleep_duration); mqtt_sn_receive_disconnect(sock); close(sock); } mqtt_sn_cleanup(); free(topic_name_ar); free(predef_topic_id_ar); return 0; } ================================================ FILE: mqtt-sn.c ================================================ /* Common functions used by the MQTT-SN Tools Copyright (C) Nicholas Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mqtt-sn.h" #ifndef AI_DEFAULT #define AI_DEFAULT (AI_ADDRCONFIG|AI_V4MAPPED) #endif static uint8_t debug = 0; static uint8_t verbose = 0; static uint8_t timeout = MQTT_SN_DEFAULT_TIMEOUT; static uint16_t next_message_id = 1; static time_t last_transmit = 0; static time_t last_receive = 0; static time_t keep_alive = 0; static uint8_t forwarder_encapsulation = FALSE; const uint8_t *wireless_node_id = NULL; uint8_t wireless_node_id_len = 0; topic_map_t *topic_map = NULL; void mqtt_sn_set_debug(uint8_t value) { debug = value; mqtt_sn_log_debug("Debug level is: %d.", debug); } void mqtt_sn_set_verbose(uint8_t value) { verbose = value; mqtt_sn_log_debug("Verbose level is: %d.", verbose); } void mqtt_sn_set_timeout(uint8_t value) { if (value < 1) { timeout = MQTT_SN_DEFAULT_TIMEOUT; } else { timeout = value; } mqtt_sn_log_debug("Network timeout is: %d seconds.", timeout); } int mqtt_sn_create_socket(const char* host, const char* port, uint16_t source_port) { struct addrinfo hints; struct addrinfo *result, *rp; struct timeval tv; int fd, ret; // Set options for the resolver memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */ hints.ai_flags = 0; hints.ai_protocol = 0; /* Any protocol */ hints.ai_canonname = NULL; hints.ai_addr = NULL; hints.ai_next = NULL; // Lookup address ret = getaddrinfo(host, port, &hints, &result); if (ret != 0) { mqtt_sn_log_err("getaddrinfo: %s", gai_strerror(ret)); exit(EXIT_FAILURE); } /* getaddrinfo() returns a list of address structures. Try each address until we successfully connect(2). If socket(2) (or connect(2)) fails, we (close the socket and) try the next address. */ for (rp = result; rp != NULL; rp = rp->ai_next) { char hoststr[NI_MAXHOST] = ""; int error = 0; // Display the IP address in debug mode error = getnameinfo(rp->ai_addr, rp->ai_addrlen, hoststr, sizeof(hoststr), NULL, 0, NI_NUMERICHOST | NI_NUMERICSERV); if (error == 0) { mqtt_sn_log_debug("Trying %s...", hoststr); } else { mqtt_sn_log_debug("getnameinfo: %s", gai_strerror(ret)); } // Create a socket fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd == -1) { mqtt_sn_log_debug("Failed to create socket: %s", strerror(errno)); continue; } if (source_port != 0) { // Bind socket to the correct port struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = rp->ai_family; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(source_port); if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { mqtt_sn_log_debug("Failed to bind socket: %s", strerror(errno)); continue; } } // Connect socket to the remote host if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { // Success break; } else { mqtt_sn_log_debug("Connect failed: %s", strerror(errno)); } close(fd); } if (rp == NULL) { mqtt_sn_log_err("Could not connect to remote host."); exit(EXIT_FAILURE); } freeaddrinfo(result); // FIXME: set the Don't Fragment flag // Setup timeout on the socket tv.tv_sec = timeout; tv.tv_usec = 0; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { perror("Error setting timeout on socket"); } return fd; } void mqtt_sn_send_packet(int sock, const void* data) { ssize_t sent = 0; size_t len = ((uint8_t*)data)[0]; // If forwarder encapsulation enabled, wrap packet if (forwarder_encapsulation) { return mqtt_sn_send_frwdencap_packet(sock, data, wireless_node_id, wireless_node_id_len); } if (debug > 1) { mqtt_sn_log_debug("Sending %2lu bytes. Type=%s on Socket: %d.", (long unsigned int)len, mqtt_sn_type_string(((uint8_t*)data)[1]), sock); } sent = send(sock, data, len, 0); if (sent != len) { mqtt_sn_log_warn("Only sent %d of %d bytes", (int)sent, (int)len); } // Store the last time that we sent a packet last_transmit = time(NULL); } void mqtt_sn_send_frwdencap_packet(int sock, const void* data, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len) { ssize_t sent = 0; size_t len = ((uint8_t*)data)[0]; uint8_t orig_packet_type = ((uint8_t*)data)[1]; frwdencap_packet_t *packet; packet = mqtt_sn_create_frwdencap_packet(data, &len, wireless_node_id, wireless_node_id_len); if (debug > 1) { mqtt_sn_log_debug("Sending %2lu bytes. Type=%s with %s inside on Socket: %d.", (long unsigned int)len, mqtt_sn_type_string(packet->type), mqtt_sn_type_string(orig_packet_type), sock); } sent = send(sock, packet, len, 0); if (sent != len) { mqtt_sn_log_debug("Warning: only sent %d of %d bytes.", (int)sent, (int)len); } // Store the last time that we sent a packet last_transmit = time(NULL); free(packet); } uint8_t mqtt_sn_validate_packet(const void *packet, size_t length) { const uint8_t* buf = packet; if (buf[0] == 0x00) { mqtt_sn_log_warn("Packet length header is not valid"); return FALSE; } if (buf[0] == 0x01) { mqtt_sn_log_warn("Packet received is longer than this tool can handle"); return FALSE; } // When forwarder encapsulation is enabled each packet must be FRWDENCAP type if (forwarder_encapsulation && buf[1] != MQTT_SN_TYPE_FRWDENCAP) { mqtt_sn_log_warn("Expecting FRWDENCAP packet and got Type=%s.", mqtt_sn_type_string(buf[1])); return FALSE; } // If packet is forwarder encapsulation expected packet length is sum of forwarder encapsulation // header and length of encapsulated packet. if ((buf[1] == MQTT_SN_TYPE_FRWDENCAP && buf[0] + buf[buf[0]] != length) || (buf[1] != MQTT_SN_TYPE_FRWDENCAP && buf[0] != length)) { mqtt_sn_log_warn("Read %d bytes but packet length is %d bytes.", (int)length, buf[1] != MQTT_SN_TYPE_FRWDENCAP ? (int)buf[0] : (int)(buf[0] + buf[buf[0]])); return FALSE; } return TRUE; } void* mqtt_sn_receive_packet(int sock) { uint8_t *wireless_node_id = NULL; uint8_t wireless_node_id_len = 0; return mqtt_sn_receive_frwdencap_packet(sock, &wireless_node_id, &wireless_node_id_len); } void* mqtt_sn_receive_frwdencap_packet(int sock, uint8_t **wireless_node_id, uint8_t *wireless_node_id_len) { static uint8_t buffer[MQTT_SN_MAX_PACKET_LENGTH + MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH + 3 + 1]; struct sockaddr_storage addr; socklen_t slen = sizeof(addr); uint8_t *packet = buffer; ssize_t bytes_read; *wireless_node_id = NULL; *wireless_node_id_len = 0; mqtt_sn_log_debug("waiting for packet..."); // Read in the packet bytes_read = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, &slen); if (bytes_read < 0) { if (errno == EAGAIN) { mqtt_sn_log_debug("Timed out waiting for packet."); return NULL; } else { perror("recv failed"); exit(EXIT_FAILURE); } } // Convert the source address into a string if (debug) { char addrstr[INET6_ADDRSTRLEN] = "unknown"; uint16_t port = 0; if (addr.ss_family == AF_INET) { struct sockaddr_in *in = (struct sockaddr_in *)&addr; inet_ntop(AF_INET, &in->sin_addr, addrstr, sizeof(struct sockaddr_in)); port = ntohs(in->sin_port); } else if (addr.ss_family == AF_INET6) { struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)&addr; inet_ntop(AF_INET6, &in6->sin6_addr, addrstr, sizeof(struct sockaddr_in6)); port = ntohs(in6->sin6_port); } if (packet[1] == MQTT_SN_TYPE_FRWDENCAP) { mqtt_sn_log_debug("Received %2d bytes from %s:%d. Type=%s with %s inside on Socket: %d", (int)bytes_read, addrstr, port, mqtt_sn_type_string(buffer[1]), mqtt_sn_type_string(packet[packet[0] + 1]), sock); } else { mqtt_sn_log_debug("Received %2d bytes from %s:%d. Type=%s on Socket: %d", (int)bytes_read, addrstr, port, mqtt_sn_type_string(buffer[1]), sock); } } if (mqtt_sn_validate_packet(buffer, bytes_read) == FALSE) { return NULL; } // NULL-terminate the packet buffer[bytes_read] = '\0'; if (packet[1] == MQTT_SN_TYPE_FRWDENCAP) { *wireless_node_id = &packet[3]; *wireless_node_id_len = packet[0] - 3; // Shift packet by the actual length of FRWDENCAP header packet += packet[0]; } // Store the last time that we received a packet last_receive = time(NULL); return packet; } void mqtt_sn_send_connect(int sock, const char* client_id, uint16_t keepalive, uint8_t clean_session) { connect_packet_t packet; memset(&packet, 0, sizeof(packet)); // Check that it isn't too long if (client_id && strlen(client_id) > MQTT_SN_MAX_CLIENT_ID_LENGTH) { mqtt_sn_log_err("Client id is too long"); exit(EXIT_FAILURE); } // Create the CONNECT packet packet.type = MQTT_SN_TYPE_CONNECT; packet.flags = clean_session ? MQTT_SN_FLAG_CLEAN : 0; packet.protocol_id = MQTT_SN_PROTOCOL_ID; packet.duration = htons(keepalive); // Generate a Client ID if none given if (client_id == NULL || client_id[0] == '\0') { snprintf(packet.client_id, MQTT_SN_MAX_CLIENT_ID_LENGTH, "mqtt-sn-tools-%d", getpid()); } else { memcpy(packet.client_id, client_id, strlen(client_id)); } packet.length = 0x06 + strlen(packet.client_id); mqtt_sn_log_debug("Sending CONNECT packet..."); // Store the keep alive period if (keepalive) { keep_alive = keepalive; } mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_send_register(int sock, const char* topic_name) { size_t topic_name_len = strlen(topic_name); register_packet_t packet; memset(&packet, 0, sizeof(packet)); if (topic_name_len > MQTT_SN_MAX_TOPIC_LENGTH) { mqtt_sn_log_err("Topic name is too long"); exit(EXIT_FAILURE); } packet.type = MQTT_SN_TYPE_REGISTER; packet.topic_id = 0; packet.message_id = htons(next_message_id++); strncpy(packet.topic_name, topic_name, sizeof(packet.topic_name)); packet.length = 0x06 + topic_name_len; mqtt_sn_log_debug("Sending REGISTER packet..."); mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_send_regack(int sock, int topic_id, int mesage_id) { regack_packet_t packet; memset(&packet, 0, sizeof(packet)); packet.type = MQTT_SN_TYPE_REGACK; packet.topic_id = htons(topic_id); packet.message_id = htons(mesage_id); packet.return_code = 0x00; packet.length = 0x07; mqtt_sn_log_debug("Sending REGACK packet..."); mqtt_sn_send_packet(sock, &packet); } static uint8_t mqtt_sn_get_qos_flag(int8_t qos) { switch (qos) { case -1: return MQTT_SN_FLAG_QOS_N1; case 0: return MQTT_SN_FLAG_QOS_0; case 1: return MQTT_SN_FLAG_QOS_1; case 2: return MQTT_SN_FLAG_QOS_2; default: return 0; } } void mqtt_sn_send_publish(int sock, uint16_t topic_id, uint8_t topic_type, const void* data, uint16_t data_len, int8_t qos, uint8_t retain) { publish_packet_t packet; memset(&packet, 0, sizeof(packet)); if (data_len > sizeof(packet.data)) { mqtt_sn_log_err("Payload is too big"); exit(EXIT_FAILURE); } packet.type = MQTT_SN_TYPE_PUBLISH; packet.flags = 0x00; if (retain) packet.flags += MQTT_SN_FLAG_RETAIN; packet.flags += mqtt_sn_get_qos_flag(qos); packet.flags += (topic_type & 0x3); packet.topic_id = htons(topic_id); if (qos > 0) { packet.message_id = htons(next_message_id++); } else { packet.message_id = 0x0000; } memcpy(packet.data, data, sizeof(packet.data)); packet.length = 0x07 + data_len; mqtt_sn_log_debug("Sending PUBLISH packet..."); mqtt_sn_send_packet(sock, &packet); if (qos == 1) { // Now wait for a PUBACK puback_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_PUBACK, sock); if (packet) { mqtt_sn_log_debug("Received PUBACK"); } else { mqtt_sn_log_warn("Failed to receive PUBACK after PUBLISH"); } } } void mqtt_sn_send_puback(int sock, publish_packet_t* publish, uint8_t return_code) { puback_packet_t puback; memset(&puback, 0, sizeof(puback)); puback.type = MQTT_SN_TYPE_PUBACK; puback.topic_id = publish->topic_id; puback.message_id = publish->message_id; puback.return_code = return_code; puback.length = 0x07; mqtt_sn_log_debug("Sending PUBACK packet..."); mqtt_sn_send_packet(sock, &puback); } void mqtt_sn_send_subscribe_topic_name(int sock, const char* topic_name, uint8_t qos) { size_t topic_name_len = strlen(topic_name); subscribe_packet_t packet; memset(&packet, 0, sizeof(packet)); packet.type = MQTT_SN_TYPE_SUBSCRIBE; packet.flags = 0x00; packet.flags += mqtt_sn_get_qos_flag(qos); if (topic_name_len == 2) { packet.flags += MQTT_SN_TOPIC_TYPE_SHORT; } else { packet.flags += MQTT_SN_TOPIC_TYPE_NORMAL; } packet.message_id = htons(next_message_id++); strncpy(packet.topic_name, topic_name, sizeof(packet.topic_name)); packet.topic_name[sizeof(packet.topic_name)-1] = '\0'; packet.length = 0x05 + topic_name_len; mqtt_sn_log_debug("Sending SUBSCRIBE packet..."); mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_send_subscribe_topic_id(int sock, uint16_t topic_id, uint8_t qos) { subscribe_packet_t packet; memset(&packet, 0, sizeof(packet)); packet.type = MQTT_SN_TYPE_SUBSCRIBE; packet.flags = 0x00; packet.flags += mqtt_sn_get_qos_flag(qos); packet.flags += MQTT_SN_TOPIC_TYPE_PREDEFINED; packet.message_id = htons(next_message_id++); packet.topic_id = htons(topic_id); packet.length = 0x05 + 2; mqtt_sn_log_debug("Sending SUBSCRIBE packet..."); mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_send_pingreq(int sock) { char packet[2]; packet[0] = 2; packet[1] = MQTT_SN_TYPE_PINGREQ; mqtt_sn_log_debug("Sending PINGREQ packet..."); mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_send_disconnect(int sock, uint16_t duration) { disconnect_packet_t packet; memset(&packet, 0, sizeof(packet)); packet.type = MQTT_SN_TYPE_DISCONNECT; if (duration == 0) { packet.length = 0x02; mqtt_sn_log_debug("Sending DISCONNECT packet..."); } else { packet.length = sizeof(packet); packet.duration = htons(duration); mqtt_sn_log_debug("Sending DISCONNECT packet with Duration %d...", duration); } mqtt_sn_send_packet(sock, &packet); } void mqtt_sn_receive_disconnect(int sock) { disconnect_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_DISCONNECT, sock); if (packet == NULL) { mqtt_sn_log_err("Failed to disconnect from MQTT-SN gateway."); exit(EXIT_FAILURE); } // Check Disconnect return duration if (packet->length == 4) { mqtt_sn_log_warn("DISCONNECT warning. Gateway returned duration in disconnect packet: 0x%2.2x", packet->duration); } } void mqtt_sn_receive_connack(int sock) { connack_packet_t *packet = mqtt_sn_receive_packet(sock); if (packet == NULL) { mqtt_sn_log_err("Failed to connect to MQTT-SN gateway."); exit(EXIT_FAILURE); } if (packet->type != MQTT_SN_TYPE_CONNACK) { mqtt_sn_log_err("Was expecting CONNACK packet but received: %s", mqtt_sn_type_string(packet->type)); exit(EXIT_FAILURE); } // Check Connack return code mqtt_sn_log_debug("CONNACK return code: 0x%2.2x", packet->return_code); if (packet->return_code) { mqtt_sn_log_err("CONNECT error: %s", mqtt_sn_return_code_string(packet->return_code)); exit(packet->return_code); } } static int mqtt_sn_process_register(int sock, const register_packet_t *packet) { int message_id = ntohs(packet->message_id); int topic_id = ntohs(packet->topic_id); const char* topic_name = packet->topic_name; // Add it to the topic map mqtt_sn_register_topic(topic_id, topic_name); // Respond to gateway with REGACK mqtt_sn_send_regack(sock, topic_id, message_id); return 0; } void mqtt_sn_register_topic(int topic_id, const char* topic_name) { topic_map_t **ptr = &topic_map; // Check topic ID is valid if (topic_id == 0x0000 || topic_id == 0xFFFF) { mqtt_sn_log_err("Attempted to register invalid topic id: 0x%4.4x", topic_id); return; } // Check topic name is valid if (topic_name == NULL || strlen(topic_name) <= 0) { mqtt_sn_log_err("Attempted to register invalid topic name."); return; } mqtt_sn_log_debug("Registering topic 0x%4.4x: %s", topic_id, topic_name); // Look for the topic id while (*ptr) { if ((*ptr)->topic_id == topic_id) { break; } else { ptr = &((*ptr)->next); } } // Allocate memory for a new entry, if we reached the end of the list if (*ptr == NULL) { *ptr = (topic_map_t *)malloc(sizeof(topic_map_t)); if (!*ptr) { mqtt_sn_log_err("Failed to allocate memory for new topic map entry."); exit(EXIT_FAILURE); } (*ptr)->next = NULL; } // Copy in the name to the entry strncpy((*ptr)->topic_name, topic_name, MQTT_SN_MAX_TOPIC_LENGTH); (*ptr)->topic_id = topic_id; } const char* mqtt_sn_lookup_topic(int topic_id) { topic_map_t **ptr = &topic_map; while (*ptr) { if ((*ptr)->topic_id == topic_id) { return (*ptr)->topic_name; } ptr = &((*ptr)->next); } mqtt_sn_log_warn("Failed to lookup topic id: 0x%4.4x", topic_id); return NULL; } uint16_t mqtt_sn_receive_regack(int sock) { regack_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_REGACK, sock); uint16_t received_message_id, received_topic_id; if (packet == NULL) { mqtt_sn_log_err("Failed to connect to register topic."); exit(EXIT_FAILURE); } // Check Regack return code mqtt_sn_log_debug("REGACK return code: 0x%2.2x", packet->return_code); if (packet->return_code) { mqtt_sn_log_err("REGISTER failed: %s", mqtt_sn_return_code_string(packet->return_code)); exit(packet->return_code); } // Check that the Message ID matches received_message_id = ntohs(packet->message_id); if (received_message_id != next_message_id-1) { mqtt_sn_log_warn("Message id in Regack does not equal message id sent"); } // Return the topic ID returned by the gateway received_topic_id = ntohs(packet->topic_id); mqtt_sn_log_debug("REGACK topic id: 0x%4.4x", received_topic_id); return received_topic_id; } void mqtt_sn_dump_packet(char* packet) { printf("%s: len=%d", mqtt_sn_type_string(packet[1]), packet[0]); switch(packet[1]) { case MQTT_SN_TYPE_CONNECT: { connect_packet_t* cpkt = (connect_packet_t*)packet; printf(" protocol_id=%d", cpkt->protocol_id); printf(" duration=%d", ntohs(cpkt->duration)); printf(" client_id=%s", cpkt->client_id); break; } case MQTT_SN_TYPE_CONNACK: { connack_packet_t* capkt = (connack_packet_t*)packet; printf(" return_code=%d (%s)", capkt->return_code, mqtt_sn_return_code_string(capkt->return_code)); break; } case MQTT_SN_TYPE_REGISTER: { register_packet_t* rpkt = (register_packet_t*)packet; printf(" topic_id=0x%4.4x", ntohs(rpkt->topic_id)); printf(" message_id=0x%4.4x", ntohs(rpkt->message_id)); printf(" topic_name=%s", rpkt->topic_name); break; } case MQTT_SN_TYPE_REGACK: { regack_packet_t* rapkt = (regack_packet_t*)packet; printf(" topic_id=0x%4.4x", ntohs(rapkt->topic_id)); printf(" message_id=0x%4.4x", ntohs(rapkt->message_id)); printf(" return_code=%d (%s)", rapkt->return_code, mqtt_sn_return_code_string(rapkt->return_code)); break; } case MQTT_SN_TYPE_PUBLISH: { publish_packet_t* ppkt = (publish_packet_t*)packet; printf(" topic_id=0x%4.4x", ntohs(ppkt->topic_id)); printf(" message_id=0x%4.4x", ntohs(ppkt->message_id)); printf(" data=%s", ppkt->data); break; } case MQTT_SN_TYPE_SUBSCRIBE: { subscribe_packet_t* spkt = (subscribe_packet_t*)packet; printf(" message_id=0x%4.4x", ntohs(spkt->message_id)); break; } case MQTT_SN_TYPE_SUBACK: { suback_packet_t* sapkt = (suback_packet_t*)packet; printf(" topic_id=0x%4.4x", ntohs(sapkt->topic_id)); printf(" message_id=0x%4.4x", ntohs(sapkt->message_id)); printf(" return_code=%d (%s)", sapkt->return_code, mqtt_sn_return_code_string(sapkt->return_code)); break; } case MQTT_SN_TYPE_DISCONNECT: { disconnect_packet_t* dpkt = (disconnect_packet_t*)packet; printf(" duration=%d", ntohs(dpkt->duration)); break; } } printf("\n"); fflush(stdout); } void mqtt_sn_print_publish_packet(publish_packet_t* packet) { if (verbose) { int topic_type = packet->flags & 0x3; int topic_id = ntohs(packet->topic_id); if (verbose == 2) { time_t rcv_time; char tm_buffer [40]; time(&rcv_time) ; strftime(tm_buffer, 40, "%F %T ", localtime(&rcv_time)); fputs(tm_buffer, stdout); } switch (topic_type) { case MQTT_SN_TOPIC_TYPE_NORMAL: { const char *topic_name = mqtt_sn_lookup_topic(topic_id); if (topic_name) { printf("%s: %s\n", topic_name, packet->data); } break; }; case MQTT_SN_TOPIC_TYPE_PREDEFINED: { printf("%4.4x: %s\n", topic_id, packet->data); break; }; case MQTT_SN_TOPIC_TYPE_SHORT: { const char *str = (const char*)&packet->topic_id; printf("%c%c: %s\n", str[0], str[1], packet->data); break; }; } } else { printf("%s\n", packet->data); } fflush(stdout); } uint16_t mqtt_sn_receive_suback(int sock) { suback_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_SUBACK, sock); uint16_t received_message_id, received_topic_id; if (packet == NULL) { mqtt_sn_log_err("Failed to subscribe to topic."); exit(EXIT_FAILURE); } // Check Suback return code mqtt_sn_log_debug("SUBACK return code: 0x%2.2x", packet->return_code); if (packet->return_code) { mqtt_sn_log_err("SUBSCRIBE error: %s", mqtt_sn_return_code_string(packet->return_code)); exit(packet->return_code); } // Check that the Message ID matches received_message_id = ntohs(packet->message_id); if (received_message_id != next_message_id-1) { mqtt_sn_log_warn("Message id in SUBACK does not equal message id sent"); mqtt_sn_log_debug(" Expecting: %d", next_message_id-1); mqtt_sn_log_debug(" Actual: %d", received_message_id); } // Return the topic ID returned by the gateway received_topic_id = ntohs(packet->topic_id); mqtt_sn_log_debug("SUBACK topic id: 0x%4.4x", received_topic_id); return received_topic_id; } int mqtt_sn_select(int sock) { struct timeval tv; fd_set rfd; int ret; FD_ZERO(&rfd); FD_SET(sock, &rfd); tv.tv_sec = timeout; tv.tv_usec = 0; ret = select(sock + 1, &rfd, NULL, NULL, &tv); if (ret < 0 && errno != EINTR) { // Something is wrong. perror("select"); exit(EXIT_FAILURE); } return ret; } void* mqtt_sn_wait_for(uint8_t type, int sock) { time_t started_waiting = time(NULL); while(TRUE) { time_t now = time(NULL); int ret; // Time to send a ping? if (keep_alive > 0 && (now - last_transmit) >= keep_alive) { mqtt_sn_send_pingreq(sock); } ret = mqtt_sn_select(sock); if (ret < 0) { break; } else if (ret > 0) { char* packet = mqtt_sn_receive_packet(sock); if (packet) { switch(packet[1]) { case MQTT_SN_TYPE_PUBLISH: mqtt_sn_print_publish_packet((publish_packet_t *)packet); break; case MQTT_SN_TYPE_REGISTER: mqtt_sn_process_register(sock, (register_packet_t*)packet); break; case MQTT_SN_TYPE_PINGRESP: // do nothing break; case MQTT_SN_TYPE_DISCONNECT: if (type != MQTT_SN_TYPE_DISCONNECT) { mqtt_sn_log_warn("Received DISCONNECT from gateway."); exit(EXIT_FAILURE); } break; default: if (packet[1] != type) { mqtt_sn_log_warn( "Was expecting %s packet but received: %s", mqtt_sn_type_string(type), mqtt_sn_type_string(packet[1]) ); } break; } // Did we find what we were looking for? if (packet[1] == type) { return packet; } } } // Check for receive timeout if (keep_alive > 0 && (now - last_receive) >= (keep_alive * 1.5)) { mqtt_sn_log_err("Keep alive error: timed out while waiting for a %s from gateway.", mqtt_sn_type_string(type)); exit(EXIT_FAILURE); } // Check if we have timed out waiting for the packet we are looking for if ((now - started_waiting) >= timeout) { mqtt_sn_log_debug("Timed out while waiting for a %s from gateway.", mqtt_sn_type_string(type)); break; } } return NULL; } const char* mqtt_sn_type_string(uint8_t type) { switch(type) { case MQTT_SN_TYPE_ADVERTISE: return "ADVERTISE"; case MQTT_SN_TYPE_SEARCHGW: return "SEARCHGW"; case MQTT_SN_TYPE_GWINFO: return "GWINFO"; case MQTT_SN_TYPE_CONNECT: return "CONNECT"; case MQTT_SN_TYPE_CONNACK: return "CONNACK"; case MQTT_SN_TYPE_WILLTOPICREQ: return "WILLTOPICREQ"; case MQTT_SN_TYPE_WILLTOPIC: return "WILLTOPIC"; case MQTT_SN_TYPE_WILLMSGREQ: return "WILLMSGREQ"; case MQTT_SN_TYPE_WILLMSG: return "WILLMSG"; case MQTT_SN_TYPE_REGISTER: return "REGISTER"; case MQTT_SN_TYPE_REGACK: return "REGACK"; case MQTT_SN_TYPE_PUBLISH: return "PUBLISH"; case MQTT_SN_TYPE_PUBACK: return "PUBACK"; case MQTT_SN_TYPE_PUBCOMP: return "PUBCOMP"; case MQTT_SN_TYPE_PUBREC: return "PUBREC"; case MQTT_SN_TYPE_PUBREL: return "PUBREL"; case MQTT_SN_TYPE_SUBSCRIBE: return "SUBSCRIBE"; case MQTT_SN_TYPE_SUBACK: return "SUBACK"; case MQTT_SN_TYPE_UNSUBSCRIBE: return "UNSUBSCRIBE"; case MQTT_SN_TYPE_UNSUBACK: return "UNSUBACK"; case MQTT_SN_TYPE_PINGREQ: return "PINGREQ"; case MQTT_SN_TYPE_PINGRESP: return "PINGRESP"; case MQTT_SN_TYPE_DISCONNECT: return "DISCONNECT"; case MQTT_SN_TYPE_WILLTOPICUPD: return "WILLTOPICUPD"; case MQTT_SN_TYPE_WILLTOPICRESP: return "WILLTOPICRESP"; case MQTT_SN_TYPE_WILLMSGUPD: return "WILLMSGUPD"; case MQTT_SN_TYPE_WILLMSGRESP: return "WILLMSGRESP"; case MQTT_SN_TYPE_FRWDENCAP: return "FRWDENCAP"; default: return "UNKNOWN"; } } const char* mqtt_sn_return_code_string(uint8_t return_code) { switch(return_code) { case MQTT_SN_ACCEPTED: return "Accepted"; case MQTT_SN_REJECTED_CONGESTION: return "Rejected: congestion"; case MQTT_SN_REJECTED_INVALID: return "Rejected: invalid topic ID"; case MQTT_SN_REJECTED_NOT_SUPPORTED: return "Rejected: not supported"; default: return "Rejected: unknown reason"; } } void mqtt_sn_cleanup() { topic_map_t *ptr = topic_map; topic_map_t *ptr2 = NULL; // Walk through the topic map, deleting each entry while (ptr) { ptr2 = ptr; ptr = ptr->next; free(ptr2); } topic_map = NULL; } uint8_t mqtt_sn_enable_frwdencap() { return forwarder_encapsulation = TRUE; } uint8_t mqtt_sn_disable_frwdencap() { return forwarder_encapsulation = FALSE; } void mqtt_sn_set_frwdencap_parameters(const uint8_t *wlnid, uint8_t wlnid_len) { wireless_node_id = wlnid; wireless_node_id_len = wlnid_len; } frwdencap_packet_t* mqtt_sn_create_frwdencap_packet(const void *data, size_t *len, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len) { frwdencap_packet_t* packet = NULL; // Check that it isn't too long if (wireless_node_id_len > MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH) { mqtt_sn_log_err("Wireless node id is longer than %d", MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH); exit(EXIT_FAILURE); } // Allocates a block of memory for an array of num elements, each of them size bytes long, // and initializes all its bits to zero. packet = malloc(sizeof(frwdencap_packet_t)); packet->type = MQTT_SN_TYPE_FRWDENCAP; packet->ctrl = 0; // Generate a Wireless Node ID if none given if (wireless_node_id == NULL || wireless_node_id_len == 0) { // A null character is automatically appended after the content written. snprintf((char*)packet->wireless_node_id, sizeof(packet->wireless_node_id)-1, "%X", getpid()); wireless_node_id_len = strlen((char*)packet->wireless_node_id); } else { memcpy(packet->wireless_node_id, wireless_node_id, wireless_node_id_len); } packet->length = wireless_node_id_len + 3; // Copy mqtt-sn packet into forwarder encapsulation packet memcpy(&(packet->wireless_node_id[wireless_node_id_len]), data, ((uint8_t*)data)[0]); // Set new packet length to send *len = packet->length + ((uint8_t*)data)[0]; if (debug > 2) { char wlnd[65]; char* buf_ptr = wlnd; int i; for (i = 0; i < wireless_node_id_len; i++) { buf_ptr += sprintf(buf_ptr, "%02X", packet->wireless_node_id[i]); } *(++buf_ptr) = '\0'; mqtt_sn_log_debug("Node id: 0x%s, N. id len: %d, Wrapped packet len: %d, Total len: %lu", wlnd, wireless_node_id_len, ((uint8_t*)data)[0], (long unsigned int)*len); } return packet; } static void mqtt_sn_log_msg(const char* level, const char* format, va_list arglist) { time_t mqtt_sn_log_time; char tm_buffer[40]; time(&mqtt_sn_log_time); strftime(tm_buffer, sizeof(tm_buffer), "%F %T ", localtime(&mqtt_sn_log_time)); fputs(tm_buffer, stderr); fputs(level, stderr); vfprintf(stderr, format, arglist); fputs("\n", stderr); } void mqtt_sn_log_debug(const char * format, ...) { if (debug) { va_list arglist; va_start(arglist, format); mqtt_sn_log_msg("DEBUG ", format, arglist); va_end(arglist); } } void mqtt_sn_log_warn(const char * format, ...) { va_list arglist; va_start(arglist, format); mqtt_sn_log_msg("WARN ", format, arglist); va_end(arglist); } void mqtt_sn_log_err(const char * format, ...) { va_list arglist; va_start(arglist, format); mqtt_sn_log_msg("ERROR ", format, arglist); va_end(arglist); } ================================================ FILE: mqtt-sn.h ================================================ /* Common functions used by the MQTT-SN Tools Copyright (C) Nicholas Humfrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #ifndef MQTT_SN_H #define MQTT_SN_H #ifndef FALSE #define FALSE (0) #endif #ifndef TRUE #define TRUE (1) #endif #define MQTT_SN_DEFAULT_PORT "1883" #define MQTT_SN_DEFAULT_TIMEOUT (10) #define MQTT_SN_DEFAULT_KEEP_ALIVE (10) #define MQTT_SN_MAX_PACKET_LENGTH (255) #define MQTT_SN_MAX_PAYLOAD_LENGTH (MQTT_SN_MAX_PACKET_LENGTH-7) #define MQTT_SN_MAX_TOPIC_LENGTH (MQTT_SN_MAX_PACKET_LENGTH-6) #define MQTT_SN_MAX_CLIENT_ID_LENGTH (23) #define MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH (252) #define MQTT_SN_TYPE_ADVERTISE (0x00) #define MQTT_SN_TYPE_SEARCHGW (0x01) #define MQTT_SN_TYPE_GWINFO (0x02) #define MQTT_SN_TYPE_CONNECT (0x04) #define MQTT_SN_TYPE_CONNACK (0x05) #define MQTT_SN_TYPE_WILLTOPICREQ (0x06) #define MQTT_SN_TYPE_WILLTOPIC (0x07) #define MQTT_SN_TYPE_WILLMSGREQ (0x08) #define MQTT_SN_TYPE_WILLMSG (0x09) #define MQTT_SN_TYPE_REGISTER (0x0A) #define MQTT_SN_TYPE_REGACK (0x0B) #define MQTT_SN_TYPE_PUBLISH (0x0C) #define MQTT_SN_TYPE_PUBACK (0x0D) #define MQTT_SN_TYPE_PUBCOMP (0x0E) #define MQTT_SN_TYPE_PUBREC (0x0F) #define MQTT_SN_TYPE_PUBREL (0x10) #define MQTT_SN_TYPE_SUBSCRIBE (0x12) #define MQTT_SN_TYPE_SUBACK (0x13) #define MQTT_SN_TYPE_UNSUBSCRIBE (0x14) #define MQTT_SN_TYPE_UNSUBACK (0x15) #define MQTT_SN_TYPE_PINGREQ (0x16) #define MQTT_SN_TYPE_PINGRESP (0x17) #define MQTT_SN_TYPE_DISCONNECT (0x18) #define MQTT_SN_TYPE_WILLTOPICUPD (0x1A) #define MQTT_SN_TYPE_WILLTOPICRESP (0x1B) #define MQTT_SN_TYPE_WILLMSGUPD (0x1C) #define MQTT_SN_TYPE_WILLMSGRESP (0x1D) #define MQTT_SN_TYPE_FRWDENCAP (0xFE) #define MQTT_SN_ACCEPTED (0x00) #define MQTT_SN_REJECTED_CONGESTION (0x01) #define MQTT_SN_REJECTED_INVALID (0x02) #define MQTT_SN_REJECTED_NOT_SUPPORTED (0x03) #define MQTT_SN_TOPIC_TYPE_NORMAL (0x00) #define MQTT_SN_TOPIC_TYPE_PREDEFINED (0x01) #define MQTT_SN_TOPIC_TYPE_SHORT (0x02) #define MQTT_SN_FLAG_DUP (0x1 << 7) #define MQTT_SN_FLAG_QOS_0 (0x0 << 5) #define MQTT_SN_FLAG_QOS_1 (0x1 << 5) #define MQTT_SN_FLAG_QOS_2 (0x2 << 5) #define MQTT_SN_FLAG_QOS_N1 (0x3 << 5) #define MQTT_SN_FLAG_QOS_MASK (0x3 << 5) #define MQTT_SN_FLAG_RETAIN (0x1 << 4) #define MQTT_SN_FLAG_WILL (0x1 << 3) #define MQTT_SN_FLAG_CLEAN (0x1 << 2) #define MQTT_SN_PROTOCOL_ID (0x01) typedef struct { uint8_t length; uint8_t type; uint8_t flags; uint8_t protocol_id; uint16_t duration; char client_id[MQTT_SN_MAX_CLIENT_ID_LENGTH]; } connect_packet_t; typedef struct { uint8_t length; uint8_t type; uint8_t return_code; } connack_packet_t; typedef struct { uint8_t length; uint8_t type; uint16_t topic_id; uint16_t message_id; char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; } register_packet_t; typedef struct { uint8_t length; uint8_t type; uint16_t topic_id; uint16_t message_id; uint8_t return_code; } regack_packet_t; typedef struct __attribute__((packed)) { uint8_t length; uint8_t type; uint8_t flags; uint16_t topic_id; uint16_t message_id; char data[MQTT_SN_MAX_PAYLOAD_LENGTH]; } publish_packet_t; typedef struct __attribute__((packed)) { uint8_t length; uint8_t type; uint16_t topic_id; uint16_t message_id; uint8_t return_code; } puback_packet_t; typedef struct __attribute__((packed)) { uint8_t length; uint8_t type; uint8_t flags; uint16_t message_id; union { char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; uint16_t topic_id; }; } subscribe_packet_t; typedef struct __attribute__((packed)) { uint8_t length; uint8_t type; uint8_t flags; uint16_t topic_id; uint16_t message_id; uint8_t return_code; } suback_packet_t; typedef struct { uint8_t length; uint8_t type; uint16_t duration; } disconnect_packet_t; typedef struct __attribute__((packed)) { uint8_t length; uint8_t type; uint8_t ctrl; uint8_t wireless_node_id[MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH]; char data[MQTT_SN_MAX_PACKET_LENGTH]; } frwdencap_packet_t; typedef struct topic_map { uint16_t topic_id; char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; struct topic_map *next; } topic_map_t; // Library functions int mqtt_sn_create_socket(const char* host, const char* port, uint16_t source_port); void mqtt_sn_send_connect(int sock, const char* client_id, uint16_t keepalive, uint8_t clean_session); void mqtt_sn_send_register(int sock, const char* topic_name); void mqtt_sn_send_publish(int sock, uint16_t topic_id, uint8_t topic_type, const void* data, uint16_t data_len, int8_t qos, uint8_t retain); void mqtt_sn_send_puback(int sock, publish_packet_t* publish, uint8_t return_code); void mqtt_sn_send_subscribe_topic_name(int sock, const char* topic_name, uint8_t qos); void mqtt_sn_send_subscribe_topic_id(int sock, uint16_t topic_id, uint8_t qos); void mqtt_sn_send_pingreq(int sock); void mqtt_sn_send_disconnect(int sock, uint16_t duration); void mqtt_sn_receive_disconnect(int sock); void mqtt_sn_receive_connack(int sock); uint16_t mqtt_sn_receive_regack(int sock); uint16_t mqtt_sn_receive_suback(int sock); void mqtt_sn_dump_packet(char* packet); void mqtt_sn_print_publish_packet(publish_packet_t* packet); int mqtt_sn_select(int sock); void* mqtt_sn_wait_for(uint8_t type, int sock); void mqtt_sn_register_topic(int topic_id, const char* topic_name); const char* mqtt_sn_lookup_topic(int topic_id); void mqtt_sn_cleanup(); void mqtt_sn_set_debug(uint8_t value); void mqtt_sn_set_verbose(uint8_t value); void mqtt_sn_set_timeout(uint8_t value); const char* mqtt_sn_type_string(uint8_t type); const char* mqtt_sn_return_code_string(uint8_t return_code); uint8_t mqtt_sn_validate_packet(const void *packet, size_t length); void mqtt_sn_send_packet(int sock, const void* data); void mqtt_sn_send_frwdencap_packet(int sock, const void* data, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len); void* mqtt_sn_receive_packet(int sock); void* mqtt_sn_receive_frwdencap_packet(int sock, uint8_t **wireless_node_id, uint8_t *wireless_node_id_len); // Functions to turn on and off forwarder encapsulation according to MQTT-SN Protocol Specification v1.2, // chapter 5.5 Forwarder Encapsulation. uint8_t mqtt_sn_enable_frwdencap(); uint8_t mqtt_sn_disable_frwdencap(); // Set wireless node ID and wireless node ID length void mqtt_sn_set_frwdencap_parameters(const uint8_t *wlnid, uint8_t wlnid_len); // Wrap mqtt-sn packet into a forwarder encapsulation packet frwdencap_packet_t* mqtt_sn_create_frwdencap_packet(const void *data, size_t *len, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len); void mqtt_sn_log_debug(const char * format, ...); void mqtt_sn_log_warn(const char * format, ...); void mqtt_sn_log_err(const char * format, ...); #endif ================================================ FILE: test/.ruby-version ================================================ 2.5.5 ================================================ FILE: test/Gemfile ================================================ source 'https://rubygems.org' ruby File.read('.ruby-version').chomp gem 'bundler', '>= 1.5.0' gem 'rake', '>= 0.10.0' gem 'mqtt', '>= 0.4.0' gem 'minitest', '>= 5.0.0' ================================================ FILE: test/Rakefile ================================================ #!/usr/bin/env ruby require 'rubygems' require 'rake/testtask' Rake::TestTask.new do |t| t.pattern = "*-test.rb" end task :default => :test ================================================ FILE: test/fake_server.rb ================================================ #!/usr/bin/env ruby # # This is a 'fake' MQTT-SN server to help with testing client implementations # # It behaves in the following ways: # * Responds to CONNECT with a successful CONACK # * Responds to PUBLISH by keeping a copy of the packet # * Responds to SUBSCRIBE with SUBACK and a PUBLISH to the topic # * Responds to PINGREQ with PINGRESP and keeps a count # * Responds to DISCONNECT with a DISCONNECT packet # # It has the following restrictions # * Doesn't deal with timeouts # * Only handles a single client # require 'logger' require 'socket' require 'mqtt' class MQTT::SN::FakeServer attr_reader :address, :port attr_reader :thread attr_reader :packets_received # Create a new fake MQTT server # # If no port is given, bind to a random port number # If no bind address is given, bind to localhost def initialize(port=0, bind_address='127.0.0.1') @port = port @address = bind_address @packets_received = [] end # Get the logger used by the server def logger @logger ||= Logger.new(STDOUT) end def logger=(logger) @logger = logger end # Start the thread and open the socket that will process client connections def start @thread ||= Thread.new do Thread.current.abort_on_exception = true Socket.udp_server_sockets(@address, @port) do |sockets| @address = sockets.first.local_address.ip_address @port = sockets.first.local_address.ip_port logger.info "Started a fake MQTT-SN server on #{@address}:#{@port}" Socket.udp_server_loop_on(sockets) do |data, client| response = process_packet(data) unless response.nil? response = [response] unless response.kind_of?(Enumerable) response.each do |packet| logger.debug "Replying with: #{packet.inspect}" client.reply(packet.to_s) end end end end end end # Stop the thread and close the socket def stop logger.info "Stopping fake MQTT-SN server" @thread.kill if @thread and @thread.alive? @thread = nil end # Start the server thread and wait for it to finish (possibly never) def run start begin @thread.join rescue Interrupt stop end end def wait_for_port_number while @port.to_i == 0 Thread.pass end @port end def wait_for_packet(klass=nil, timeout=3) begin Timeout.timeout(timeout) do if block_given? @packets_received = [] yield end loop do if klass.nil? unless @packets_received.empty? return @packets_received.last_packet end else @packets_received.each do |packet| return packet if packet.class == klass end end sleep(0.01) end end rescue Timeout::Error logger.warn "FakeServer timed out waiting for a #{klass}" return nil end end protected def process_packet(data) packet = MQTT::SN::Packet.parse(data) @packets_received << packet logger.debug "Received: #{packet.inspect}" method = 'handle_' + packet.class.name.split('::').last.downcase if respond_to?(method, true) send(method, packet) else logger.warn "Unhandled packet type: #{packet.class}" nil end rescue MQTT::SN::ProtocolException => e logger.warn "Protocol error: #{e}" nil end def handle_connect(packet) MQTT::SN::Packet::Connack.new(:return_code => 0x00) end def handle_publish(packet) if packet.qos > 0 MQTT::SN::Packet::Puback.new( :id => packet.id, :topic_id => packet.topic_id, :return_code => 0x00 ) end end def handle_pingreq(packet) MQTT::SN::Packet::Pingresp.new end def handle_subscribe(packet, publish_data=nil) case packet.topic_id_type when :short topic_id = packet.topic_name publish_data ||= "Message for #{packet.topic_name}" when :predefined topic_id = packet.topic_id publish_data ||= "Message for ##{packet.topic_id}" when :normal topic_id = 1 publish_data ||= "Message for #{packet.topic_name}" else logger.warn "Unknown Topic Id Type: #{packet.topic_id_type}" end [ MQTT::SN::Packet::Suback.new( :id => packet.id, :topic_id_type => packet.topic_id_type, :topic_id => topic_id, :return_code => 0 ), MQTT::SN::Packet::Publish.new( :topic_id_type => packet.topic_id_type, :topic_id => topic_id, :qos => packet.qos, :data => publish_data ) ] end def handle_disconnect(packet) MQTT::SN::Packet::Disconnect.new end def handle_register(packet) MQTT::SN::Packet::Regack.new( :id => packet.id, :topic_id => 1, :return_code => 0x00 ) end def handle_puback(packet) nil end def handle_regack(packet) nil end end if __FILE__ == $0 server = MQTT::SN::FakeServer.new(MQTT::SN::DEFAULT_PORT) server.logger.level = Logger::DEBUG server.run end ================================================ FILE: test/mqtt-sn-dump-test.rb ================================================ $:.unshift(File.dirname(__FILE__)) require 'test_helper' class MqttSnDumpTest < Minitest::Test def test_usage @cmd_result = run_cmd('mqtt-sn-dump', '-?') assert_match(/^Usage: mqtt-sn-dump/, @cmd_result[0]) end def publish_packet(port, packet) # FIXME: better way to wait until socket is open? sleep 0.2 socket = UDPSocket.new socket.connect('localhost', port) socket << packet.to_s socket.close end def publish_qos_n1_packet(port) publish_packet(port, MQTT::SN::Packet::Publish.new( :topic_id => 'TT', :topic_id_type => :short, :data => "Message for TT", :qos => -1 ) ) end def test_receive_qos_n1 @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd) end assert_equal(["Message for TT"], @cmd_result) end def test_receive_qos_n1_debug @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-d', '-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd) end assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG mqtt-sn-dump listening on port \d+/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Received 21 bytes from 127.0.0.1\:\d+. Type=PUBLISH/, @cmd_result) end def test_receive_qos_n1_verbose @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-v', '-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd) end assert_equal(["TT: Message for TT"], @cmd_result) end def test_receive_qos_n1_dump_all @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd) end assert_equal(["PUBLISH: len=21 topic_id=0x5454 message_id=0x0000 data=Message for TT"], @cmd_result) end def test_receive_qos_n1_term @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd, 'TERM') end assert_equal(["Message for TT"], @cmd_result) end def test_receive_qos_n1_hup @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-p', @port] ) do |cmd| publish_qos_n1_packet(@port) wait_for_output_then_kill(cmd, 'HUP') end assert_equal(["Message for TT"], @cmd_result) end def test_receive_connect @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Connect.new( :client_id => 'my_client_id', :keep_alive => 10 ) ) wait_for_output_then_kill(cmd) end assert_equal(["CONNECT: len=18 protocol_id=1 duration=10 client_id=my_client_id"], @cmd_result) end def test_receive_connack @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Connack.new( :return_code => 1 ) ) wait_for_output_then_kill(cmd) end assert_equal(["CONNACK: len=3 return_code=1 (Rejected: congestion)"], @cmd_result) end def test_receive_connack_not_supported @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Connack.new( :return_code => 3 ) ) wait_for_output_then_kill(cmd) end assert_equal(["CONNACK: len=3 return_code=3 (Rejected: not supported)"], @cmd_result) end def test_receive_register @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Register.new( :id => 10, :topic_id => 20, :topic_name => 'Topic Name' ) ) wait_for_output_then_kill(cmd) end assert_equal(["REGISTER: len=16 topic_id=0x0014 message_id=0x000a topic_name=Topic Name"], @cmd_result) end def test_receive_regack @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Regack.new( :id => 30, :topic_id => 40, :return_code => 0 ) ) wait_for_output_then_kill(cmd) end assert_equal(["REGACK: len=7 topic_id=0x0028 message_id=0x001e return_code=0 (Accepted)"], @cmd_result) end def test_receive_subscribe @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Subscribe.new(:id => 50) ) wait_for_output_then_kill(cmd) end assert_equal(["SUBSCRIBE: len=5 message_id=0x0032"], @cmd_result) end def test_receive_suback @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Suback.new( :id => 60, :topic_id => 70, :return_code => 0 ) ) wait_for_output_then_kill(cmd) end assert_equal(["SUBACK: len=8 topic_id=0x0046 message_id=0x003c return_code=0 (Accepted)"], @cmd_result) end def test_receive_suback_unknown_error @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Suback.new( :id => 60, :topic_id => 70, :return_code => 5 ) ) wait_for_output_then_kill(cmd) end assert_equal(["SUBACK: len=8 topic_id=0x0046 message_id=0x003c return_code=5 (Rejected: unknown reason)"], @cmd_result) end def test_receive_pingreq @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Pingreq.new) wait_for_output_then_kill(cmd) end assert_equal(["PINGREQ: len=2"], @cmd_result) end def test_receive_pingresp @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Pingresp.new) wait_for_output_then_kill(cmd) end assert_equal(["PINGRESP: len=2"], @cmd_result) end def test_receive_disconnect @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, MQTT::SN::Packet::Disconnect.new) wait_for_output_then_kill(cmd) end assert_equal(["DISCONNECT: len=2 duration=0"], @cmd_result) end def test_receive_unknown @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, "\x03\xFF\x00") wait_for_output_then_kill(cmd) end assert_equal(["UNKNOWN: len=3"], @cmd_result) end def test_receive_invalid_length @port = random_port @cmd_result = run_cmd( 'mqtt-sn-dump', ['-a', '-p', @port] ) do |cmd| publish_packet(@port, "\x00\x00\x00\x00") wait_for_output_then_kill(cmd) end assert_match(/WARN Packet length header is not valid/, @cmd_result[0]) end end ================================================ FILE: test/mqtt-sn-pub-test.rb ================================================ $:.unshift(File.dirname(__FILE__)) require 'test_helper' class MqttSnPubTest < Minitest::Test def test_usage @cmd_result = run_cmd('mqtt-sn-pub', '-?') assert_match(/^Usage: mqtt-sn-pub/, @cmd_result[0]) end def test_no_arguments @cmd_result = run_cmd('mqtt-sn-pub') assert_match(/^Usage: mqtt-sn-pub/, @cmd_result[0]) end def test_default_client_id fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_match(/^mqtt-sn-tools-(\d+)$/, @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_custom_client_id fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-i' => 'test_custom_client_id', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal('test_custom_client_id', @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_23char_client_id fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-i' => 'ABCDEFGHIJKLMNOPQRSTUVW', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal('ABCDEFGHIJKLMNOPQRSTUVW', @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_connack_congestion fake_server do |fs| def fs.handle_connect(packet) MQTT::SN::Packet::Connack.new(:return_code => 0x01) end fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_match(/CONNECT error: Rejected: congestion/, @cmd_result[0]) end def test_no_connack fake_server do |fs| def fs.handle_connect(packet) MQTT::SN::Packet::Disconnect.new end fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_match(/ERROR Was expecting CONNACK packet but received: DISCONNECT/, @cmd_result[0]) end def test_too_long_client_id fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-pub', '-i' => 'C' * 255, '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end assert_match(/ERROR Client id is too long/, @cmd_result[0]) end def test_publish_qos_n1 fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => -1, '-T' => 10, '-m' => 'test_publish_qos_n1', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal(10, @packet.topic_id) assert_equal(:predefined, @packet.topic_id_type) assert_equal('test_publish_qos_n1', @packet.data) assert_equal(-1, @packet.qos) assert_equal(false, @packet.retain) assert_equal(0, @packet.id) end def test_publish_debug fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-d', '-t', 'topic', '-m', 'test_publish_qos_0_debug', '-p', fs.port, '-h', fs.address] ) end end assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 1/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending PUBLISH packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) end def test_publish_debug_2 fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-d', '-d', '-i', 'fixed_client_id', '-t', 'topic', '-m', 'test_publish_qos_0_debug', '-p', fs.port, '-h', fs.address] ) end end assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 2/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) assert_includes_match(/Sending 21 bytes\. Type=CONNECT on Socket: 3/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) assert_includes_match(/Received 3 bytes from 127.0.0.1\:\d+. Type=CONNACK on Socket/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending PUBLISH packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) end def test_publish_from_file fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-f', 'test.txt', '-p', fs.port, '-h', fs.address] ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal('The is the contents of test.txt', @packet.data) assert_equal(0, @packet.qos) end def test_publish_from_binary_file ## Test for a file that contains null bytes fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-f', 'test.bin', '-p', fs.port, '-h', fs.address] ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal(12, @packet.data.length) assert_equal("\x01\x02\x03\x04\x05\x00Hello\x00", @packet.data) assert_equal(0, @packet.qos) end def test_publish_from_file_too_big fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-f', 'test_big.txt', '-p', fs.port, '-h', fs.address] ) end end assert_match(/WARN Input file is longer than the maximum message size/, @cmd_result[0]) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal(248, @packet.data.length) assert_equal(0, @packet.qos) end def test_publish_from_file_hyphen fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-f', '-', '-p', fs.port, '-h', fs.address], 'Message from file -' ) end end assert_empty(@cmd_result) assert_equal('Message from file -', @packet.data) end def test_publish_from_stdin fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-s', '-p', fs.port, '-h', fs.address], 'Message from STDIN' ) end end assert_empty(@cmd_result) assert_equal('Message from STDIN', @packet.data) end def test_publish_multiline_from_stdin server = fake_server do |fs| fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-l', '-p', fs.port, '-h', fs.address], "Message 1\nMessage 2\nMessage 3\n" ) end end publish_packets = server.packets_received.select do |packet| packet.is_a?(MQTT::SN::Packet::Publish) end assert_empty(@cmd_result) assert_equal(['Message 1', 'Message 2', 'Message 3'], publish_packets.map {|p| p.data}) assert_equal([1, 1, 1], publish_packets.map {|p| p.topic_id}) assert_equal([:normal, :normal, :normal], publish_packets.map {|p| p.topic_id_type}) assert_equal([0, 0, 0], publish_packets.map {|p| p.qos}) end def test_publish_multiline_from_stdin_no_newline server = fake_server do |fs| fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-l', '-p', fs.port, '-h', fs.address], "Message 1\nMessage 2" ) end end publish_packets = server.packets_received.select do |packet| packet.is_a?(MQTT::SN::Packet::Publish) end assert_includes_match(/Failed to find newline when reading message/, @cmd_result) assert_equal(['Message 1'], publish_packets.map {|p| p.data}) assert_equal([1], publish_packets.map {|p| p.topic_id}) assert_equal([:normal], publish_packets.map {|p| p.topic_id_type}) assert_equal([0], publish_packets.map {|p| p.qos}) end def test_publish_qos_0 fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => 0, '-t' => 'topic', '-m' => 'test_publish_qos_0', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal('test_publish_qos_0', @packet.data) assert_equal(0, @packet.qos) assert_equal(false, @packet.retain) assert_equal(0, @packet.id) end def test_publish_qos_0_short fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => 0, '-t' => 'TT', '-m' => 'test_publish_qos_0_short', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal('TT', @packet.topic_id) assert_equal(:short, @packet.topic_id_type) assert_equal('test_publish_qos_0_short', @packet.data) assert_equal(0, @packet.qos) assert_equal(false, @packet.retain) assert_equal(0, @packet.id) end def test_publish_qos_0_predefined fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => 0, '-T' => 127, '-m' => 'test_publish_qos_0_predefined', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal(127, @packet.topic_id) assert_equal(:predefined, @packet.topic_id_type) assert_equal('test_publish_qos_0_predefined', @packet.data) assert_equal(0, @packet.qos) assert_equal(false, @packet.retain) assert_equal(0, @packet.id) end def test_publish_qos_0_retained fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-r', '-t', 'topic', '-m', 'test_publish_retained', '-p', fs.port, '-h', fs.address] ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal('test_publish_retained', @packet.data) assert_equal(0, @packet.qos) assert_equal(true, @packet.retain) assert_equal(0, @packet.id) end def test_publish_qos_0_empty fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-r', '-n', '-t', 'topic', '-p', fs.port, '-h', fs.address] ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal('', @packet.data) assert_equal(0, @packet.qos) assert_equal(true, @packet.retain) assert_equal(0, @packet.id) end def test_publish_qos_1 fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => 1, '-t' => 'topic', '-m' => 'test_publish_qos_1', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal(1, @packet.topic_id) assert_equal(:normal, @packet.topic_id_type) assert_equal('test_publish_qos_1', @packet.data) assert_equal(1, @packet.qos) assert_equal(false, @packet.retain) assert_equal(2, @packet.id) # REGISTER for topic ID has id 1 end def test_publish_qos_1_puback_timeout fake_server do |fs| def fs.handle_publish(packet) nil end @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => 1, '-d' => '', '-k' => 2, '-t' => 'topic', '-m' => 'test_publish_qos_1', '-p' => fs.port, '-h' => fs.address ) end end assert_includes_match(/Timed out while waiting for a PUBACK from gateway/, @cmd_result) end def test_publish_ipv6 unless have_ipv6? skip("IPv6 is not available on this system") end server = fake_server(nil, '::1') do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do @cmd_result = run_cmd( 'mqtt-sn-pub', ['-d', '-d', '-t', 'topic', '-m', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_includes_match(/Received 3 bytes from ::1:#{server.port}/, @cmd_result) assert_equal('test', @packet.data) end def test_invalid_qos @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => '2', '-t' => 'topic', '-m' => 'message' ) assert_match(/Only QoS level 0, 1 or -1 is supported/, @cmd_result[0]) end def test_payload_too_big fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-m', 'm' * 255, '-p', fs.port, '-h', fs.address] ) end assert_match(/Payload is too big/, @cmd_result[0]) end def test_both_topic_name_and_id @cmd_result = run_cmd( 'mqtt-sn-pub', '-t' => 'topic_name', '-T' => 10, '-m' => 'message' ) assert_match(/Please provide either a topic id or a topic name, not both/, @cmd_result[0]) end def test_both_message_and_file @cmd_result = run_cmd( 'mqtt-sn-pub', '-t' => 'topic_name', '-m' => 'message', '-f' => '/dev/zero' ) assert_match(/Please provide either message data or a message file, not both/, @cmd_result[0]) end def test_file_doesnt_exist fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-pub', '-t' => 'topic_name', '-f' => '/doesnt/exist', '-p' => fs.port, '-h' => fs.address ) do |cmd| wait_for_output_then_kill(cmd) end end assert_match(/Failed to open message file/, @cmd_result[0]) end def test_both_qos_n1_topic_name @cmd_result = run_cmd( 'mqtt-sn-pub', '-q' => -1, '-t' => 'topic_name', '-m' => 'message' ) assert_match(/Either a pre-defined topic id or a short topic name must be given for QoS -1/, @cmd_result[0]) end def test_topic_name_too_long fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'x' * 255, '-m', 'message', '-p', fs.port, '-h', fs.address] ) do |cmd| wait_for_output_then_kill(cmd) end end assert_match(/ERROR Topic name is too long/, @cmd_result[0]) end def test_register_invalid_topic_name fake_server do |fs| def fs.handle_register(packet) MQTT::SN::Packet::Regack.new( :id => packet.id, :return_code => 2 ) end @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', '/!invalid%topic"name', '-m', 'message', '-p', fs.port, '-h', fs.address] ) do |cmd| wait_for_output_then_kill(cmd) end end assert_match(/ERROR REGISTER failed: Rejected: invalid topic ID/, @cmd_result[0]) end def test_connect_fail skip("Unable to get this test to run reliably") # @cmd_result = run_cmd( # 'mqtt-sn-pub', # ['-t', 'topic', # '-m', 'message', # '-h', '0.0.0.1', # '-p', '29567'] # ) do |cmd| # wait_for_output_then_kill(cmd) # end # # assert_match(/ERROR Could not connect to remote host/, @cmd_result[0]) end def test_hostname_lookup_fail @cmd_result = run_cmd( 'mqtt-sn-pub', ['-t', 'topic', '-m', 'message', '-p', '29567', '-h', '!(invalid)'] ) do |cmd| wait_for_output_then_kill(cmd) end assert_match(/nodename nor servname provided, or not known|Name or service not known/, @cmd_result[0]) end def test_disconnect_after_publish fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_kind_of(MQTT::SN::Packet::Disconnect, @packet) assert_nil(@packet.duration) end def test_disconnect_after_publish_with_sleep fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-pub', '-e' => 3600, '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end end assert_empty(@cmd_result) assert_equal(MQTT::SN::Packet::Disconnect, @packet.class) assert_equal(3600, @packet.duration) end def test_disconnect_duration_warning fake_server do |fs| def fs.handle_disconnect(packet) MQTT::SN::Packet::Disconnect.new( :duration => 10 ) end @cmd_result = run_cmd( 'mqtt-sn-pub', '-T' => 10, '-m' => 'message', '-p' => fs.port, '-h' => fs.address ) end assert_match(/DISCONNECT warning. Gateway returned duration in disconnect packet/, @cmd_result[0]) end end ================================================ FILE: test/mqtt-sn-serial-bridge-test.rb ================================================ $:.unshift(File.dirname(__FILE__)) require 'test_helper' class MqttSnSerialBridgeTest < Minitest::Test def test_usage @cmd_result = run_cmd('mqtt-sn-serial-bridge', '-?') assert_match(/^Usage: mqtt-sn-serial-bridge/, @cmd_result[0]) end end ================================================ FILE: test/mqtt-sn-sub-test.rb ================================================ $:.unshift(File.dirname(__FILE__)) require 'test_helper' class MqttSnSubTest < Minitest::Test def test_usage @cmd_result = run_cmd('mqtt-sn-sub', '-?') assert_match(/^Usage: mqtt-sn-sub/, @cmd_result[0]) end def test_no_arguments @cmd_result = run_cmd('mqtt-sn-sub') assert_match(/^Usage: mqtt-sn-sub/, @cmd_result[0]) end def test_default_client_id fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_match(/^mqtt-sn-tools-(\d+)$/, @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_custom_client_id fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-i', 'test_custom_client_id', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_equal('test_custom_client_id', @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_custom_keep_alive fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-k', 5, '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_match(/^mqtt-sn-tools/, @packet.client_id) assert_equal(5, @packet.keep_alive) assert_equal(true, @packet.clean_session) end def test_connack_timeout fake_server do |fs| def fs.handle_connect(packet) nil end fs.wait_for_packet(MQTT::SN::Packet::Connect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-d', '-k', 2, '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_includes_match(/Timed out waiting for packet/, @cmd_result) end def test_subscribe_one fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_one_debug fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-d', '-1', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 1/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending SUBSCRIBE packet/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG SUBACK return code: 0x00/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) end def test_subscribe_one_verbose fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["test: Message for test"], @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_one_verbose_time fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-V', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(1, @cmd_result.count) assert_match(/\d{4}\-\d{2}\-\d{2} \d{2}\:\d{2}\:\d{2} test: Message for test/, @cmd_result[0]) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_one_short fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-v', '-t', 'tt', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["tt: Message for tt"], @cmd_result) assert_equal('tt', @packet.topic_name) assert_equal(:short, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_one_predefined fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-v', '-T', 17, '-p', fs.port, '-h', fs.address] ) end end assert_equal(["0011: Message for #17"], @cmd_result) assert_nil(@packet.topic_name) assert_equal(17, @packet.topic_id) assert_equal(:predefined, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_then_int fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd, 'INT') end end assert_equal(["test: Message for test"], @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_then_term fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd, 'TERM') end end assert_equal(["test: Message for test"], @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_then_hup fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd, 'HUP') end end assert_equal(["test: Message for test"], @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_two_topic_names fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-q', 1, '-t', 'test1', '-t', 'test2', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) wait_for_output_then_kill(cmd, 'INT') end end assert_equal(2, @cmd_result.length) assert_equal("test1: Message for test1", @cmd_result[0]) assert_equal("test2: Message for test2", @cmd_result[1]) end def test_subscribe_multiple_topics fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-q', 1, '-t', 'name', '-t', 'TT', '-T', 0x10, '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) wait_for_output_then_kill(cmd, 'INT') end end assert_equal(3, @cmd_result.length) assert_equal("name: Message for name", @cmd_result[0]) assert_equal("TT: Message for TT", @cmd_result[1]) assert_equal("0010: Message for #16", @cmd_result[2]) end def test_subscribe_thirty_topics topics = (1..30).map { |t| ['-t', "topic#{t}"] } fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-q', 1, topics, '-p', fs.port, '-h', fs.address].compact ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) wait_for_output_then_kill(cmd, 'INT') end end assert_equal(30, @cmd_result.length) assert_equal("topic1: Message for topic1", @cmd_result[0]) assert_equal("topic30: Message for topic30", @cmd_result[29]) end def test_subscribe_invalid_topic_id fake_server do |fs| def fs.handle_subscribe(packet) MQTT::SN::Packet::Suback.new( :id => packet.id, :topic_id => 0, :topic_id_type => packet.topic_id_type, :return_code => 0x02 ) end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-T', 123, '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd, 'HUP') end end assert_includes_match(/ERROR SUBSCRIBE error: Rejected: invalid topic ID/, @cmd_result) end def test_subscribe_one_qos1 fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-q', 1, '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_equal(1, @packet.topic_id) assert_equal(0, @packet.id) end def test_subscribe_invalid_connack_packet fake_server do |fs| def fs.handle_connect(packet) "\x00\x05\x00" end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-T', 123, '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_includes_match(/Packet length header is not valid/, @cmd_result) end def test_subscribe_incorrect_connack_packet_length fake_server do |fs| def fs.handle_connect(packet) "\x04\x05\x00" end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-T', 123, '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_includes_match(/Read 3 bytes but packet length is 4 bytes/, @cmd_result) end def test_subscribe_id_mismatch fake_server do |fs| def fs.handle_subscribe(packet) MQTT::SN::Packet::Suback.new( :id => 222, :topic_id => 0, :topic_id_type => packet.topic_id_type, :return_code => 0x00 ) end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-d', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd) end end assert_includes_match(/WARN Message id in SUBACK does not equal message id sent/, @cmd_result) assert_includes_match(/DEBUG Expecting: 1/, @cmd_result) assert_includes_match(/DEBUG Actual: 222/, @cmd_result) end def test_subscribe_then_interupt_debug fake_server do |fs| @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-d', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd) end end assert_includes_match(/DEBUG Debug level is: 1/, @cmd_result) assert_includes_match(/DEBUG Got interrupt signal/, @cmd_result) assert_includes_match(/^test: Message for test$/, @cmd_result) assert_equal('test', @packet.topic_name) assert_equal(:normal, @packet.topic_id_type) assert_equal(0, @packet.qos) end def test_subscribe_no_clean_session @fs = fake_server do |fs| def fs.handle_connect(packet) [ MQTT::SN::Packet::Connack.new(:return_code => 0x00), MQTT::SN::Packet::Register.new(:topic_id => 5, :topic_name => 'old_topic'), MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'old_msg') ] end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-c', '-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_equal(["old_topic: old_msg", "test: Message for test"], @cmd_result) assert_match(/^mqtt-sn-tools/, @packet.client_id) assert_equal(10, @packet.keep_alive) assert_equal(false, @packet.clean_session) end def test_register_invalid_topic_id @fs = fake_server do |fs| def fs.handle_connect(packet) [ MQTT::SN::Packet::Connack.new(:return_code => 0x00), MQTT::SN::Packet::Register.new(:topic_id => 0, :topic_name => 'old_topic') ] end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-c', '-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_match(/ERROR Attempted to register invalid topic id: 0x0000/, @cmd_result[0]) assert_match(/test: Message for test/, @cmd_result[1]) end def test_register_invalid_topic_name @fs = fake_server do |fs| def fs.handle_connect(packet) [ MQTT::SN::Packet::Connack.new(:return_code => 0x00), MQTT::SN::Packet::Register.new(:topic_id => 5, :topic_name => ''), MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'old_msg') ] end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-c', '-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_match(/ERROR Attempted to register invalid topic name/, @cmd_result[0]) assert_match(/WARN Failed to lookup topic id: 0x0005/, @cmd_result[1]) assert_match(/test: Message for test/, @cmd_result[2]) end def test_recieve_non_registered_topic_id @fs = fake_server do |fs| def fs.handle_connect(packet) [ MQTT::SN::Packet::Connack.new(:return_code => 0x00), MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'not registered') ] end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-c', '-v', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) wait_for_output_then_kill(cmd) end end assert_match(/Failed to lookup topic id: 0x0005/, @cmd_result[0]) assert_match(/test: Message for test/, @cmd_result[1]) end def test_packet_too_long fake_server do |fs| def fs.handle_subscribe(packet) super(packet, 'x' * 256) end @cmd_result = run_cmd( 'mqtt-sn-sub', ['-v', '-d', '-t', 'test', '-p', fs.port, '-h', fs.address] ) do |cmd| @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) wait_for_output_then_kill(cmd) end end assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Received 265 bytes from/, @cmd_result) assert_includes_match(/[\d\-]+ [\d\:]+ WARN Packet received is longer than this tool can handle/, @cmd_result) end def test_disconnect_after_recieve fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_kind_of(MQTT::SN::Packet::Disconnect, @packet) assert_nil(@packet.duration) end def test_disconnect_after_recieve_with_sleep fake_server do |fs| @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do @cmd_result = run_cmd( 'mqtt-sn-sub', ['-1', '-e', 3600, '-t', 'test', '-p', fs.port, '-h', fs.address] ) end end assert_equal(["Message for test"], @cmd_result) assert_equal(MQTT::SN::Packet::Disconnect, @packet.class) assert_equal(3600, @packet.duration) end end ================================================ FILE: test/test.txt ================================================ The is the contents of test.txt ================================================ FILE: test/test_big.txt ================================================ 00000000 f3 a7 4f ee 23 58 28 e8 48 df bc 96 42 6a 45 e6 |..O.#X(.H...BjE.| 00000010 8e ca 44 51 7d 3a 08 80 1f b8 58 fc 7c ca e6 37 |..DQ}:....X.|..7| 00000020 ac 71 28 aa d8 a7 b8 77 76 dc 5f 80 f0 c9 1e 46 |.q(....wv._....F| 00000030 5b 5b 89 5e 6e a5 22 f1 13 cd 1f ba 1a 5f 5b 5e |[[.^n."......_[^| 00000040 73 34 90 b3 ad c7 72 61 24 32 bb c1 21 f4 a7 8a |s4....ra$2..!...| 00000050 7f 15 68 d7 61 9c 68 fd 80 ef 48 ba 25 a6 8b f3 |..h.a.h...H.%...| 00000060 5d a9 ea 86 ac da 7b 21 82 be a7 3d 2c f7 f4 dd |].....{!...=,...| 00000070 52 79 53 4c e6 6f 35 32 80 92 92 7a 48 f8 98 ca |RySL.o52...zH...| 00000080 a2 79 df f5 4b c6 7c 43 5b c2 da f3 10 2e da 53 |.y..K.|C[......S| 00000090 09 fb 96 bb 9f a7 48 8e 27 c1 1d 51 32 59 a3 2e |......H.'..Q2Y..| 000000a0 5c de 4a 4d d2 8d a8 d7 6b 00 8d 63 a9 3d d1 1c |\.JM....k..c.=..| 000000b0 73 78 db 94 94 82 f6 f6 2e e0 16 ca e9 52 2b 89 |sx...........R+.| 000000c0 b0 8d 21 22 e1 a7 2a 26 12 9a c5 97 25 da c9 17 |..!"..*&....%...| 000000d0 ad 96 55 f2 a1 75 0d 8a be ed 13 41 51 74 c2 10 |..U..u.....AQt..| 000000e0 2e 2e cf 49 26 00 36 0a c4 ce 17 46 10 06 e9 f9 |...I&.6....F....| 000000f0 ad 6b 44 4e 82 8c 8c 79 fc 27 26 06 f9 f1 24 39 |.kDN...y.'&...$9| 00000100 e0 62 3a 3a 7d d0 78 cc 95 24 de f9 c2 b3 16 60 |.b::}.x..$.....`| ================================================ FILE: test/test_helper.rb ================================================ $:.unshift(File.join(File.dirname(__FILE__),'..','lib')) require 'rubygems' require 'bundler' Bundler.require(:default) require 'minitest/autorun' require 'fake_server' CMD_DIR = File.realpath('../..', __FILE__) def run_cmd(name, args=[], data=nil) args = [args] unless args.respond_to?(:flatten) cmd = [CMD_DIR + '/' + name] + args.flatten.map {|i| i.to_s} IO.popen([*cmd, :err => [:child, :out]], 'r+b') do |io| if block_given? yield(io) end unless data.nil? io.write data end io.close_write io.readlines.map {|line| line.strip} end end def wait_for_output_then_kill(io, signal='INT', timeout=0.5) IO.select([io], nil, nil, timeout) sleep(0.1) Process.kill(signal, io.pid) end def random_port 10000 + ((rand(10000) + Time.now.to_i) % 10000) end def fake_server(*args) fs = MQTT::SN::FakeServer.new(*args) fs.logger.level = Logger::WARN fs.start fs.wait_for_port_number yield(fs) fs.stop return fs end def have_ipv6? Socket.ip_address_list.any? { |addr| addr.ipv6? } end module Minitest::Assertions def assert_includes_match(regexp, array, msg=nil) msg = message(msg) { "Expected #{mu_pp(array)} to match #{mu_pp(regexp)}" } array.each do |item| if item =~ regexp assert true, msg return end end assert false, msg end end