├── .astylerc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── NEWS.md ├── README.md ├── doc └── MQTT-SN_spec_v1.2.pdf ├── 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 ├── Gemfile.lock ├── 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.bin ├── test.txt ├── test_big.txt └── test_helper.rb /.astylerc: -------------------------------------------------------------------------------- 1 | # use the K&R code style 2 | --style=kr 3 | 4 | # Indent 4 spaces 5 | --indent=spaces=4 6 | 7 | # Indent switch and case statements 8 | --indent-switches 9 | 10 | # We don't need .orig files, because we are version controlled 11 | --suffix=none 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mqtt-sn-dump 2 | mqtt-sn-pub 3 | mqtt-sn-sub 4 | mqtt-sn-serial-bridge 5 | *.exe 6 | *.o 7 | *.tar.gz 8 | 9 | # Code coverage files 10 | *.gcda 11 | *.gcno 12 | coverage/* 13 | 14 | # Eclipse project metafiles 15 | .settings 16 | .cproject 17 | .project 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # We use ruby for running the tests 2 | language: ruby 3 | sudo: false 4 | 5 | before_install: 6 | - gem update bundler 7 | rvm: 8 | - '2.5.5' 9 | 10 | script: 11 | - make 12 | - make test 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) Nicholas J Humfrey 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=cc 2 | PACKAGE=mqtt-sn-tools 3 | VERSION=0.0.7 4 | CFLAGS=-g -Wall -DVERSION=$(VERSION) 5 | LDFLAGS= 6 | INSTALL?=install 7 | prefix=/usr/local 8 | 9 | TARGETS=mqtt-sn-dump mqtt-sn-pub mqtt-sn-sub mqtt-sn-serial-bridge 10 | 11 | .PHONY : all install uninstall clean dist test coverage 12 | 13 | 14 | all: $(TARGETS) 15 | 16 | $(TARGETS): %: mqtt-sn.o %.o 17 | $(CC) $(LDFLAGS) -o $@ $^ 18 | 19 | %.o : %.c mqtt-sn.h 20 | $(CC) $(CFLAGS) -c $< 21 | 22 | install: $(TARGETS) 23 | $(INSTALL) -d "$(DESTDIR)$(prefix)/bin" 24 | $(INSTALL) -s $(TARGETS) "$(DESTDIR)$(prefix)/bin" 25 | 26 | uninstall: 27 | @for target in $(TARGETS); do \ 28 | cmd="rm -f $(DESTDIR)$(prefix)/bin/$$target"; \ 29 | echo "$$cmd" && $$cmd; \ 30 | done 31 | 32 | clean: 33 | -rm -f *.o *.gcda *.gcno $(TARGETS) 34 | -rm -Rf coverage 35 | 36 | dist: 37 | distdir='$(PACKAGE)-$(VERSION)'; mkdir $$distdir || exit 1; \ 38 | list=`git ls-files`; for file in $$list; do \ 39 | cp -pR $$file $$distdir || exit 1; \ 40 | done; \ 41 | tar -zcf $$distdir.tar.gz $$distdir; \ 42 | rm -fr $$distdir 43 | 44 | test: all 45 | @(which bundle > /dev/null) || (echo "Ruby Bundler is not installed"; exit -1) 46 | cd test && bundle install && bundle exec rake test 47 | 48 | # Use gcc for coverage report - it works better than clang/llvm 49 | coverage: CC=gcc 50 | coverage: CFLAGS += --coverage 51 | coverage: LDFLAGS += --coverage 52 | coverage: clean test 53 | mkdir -p coverage 54 | lcov \ 55 | --capture \ 56 | --directory . \ 57 | --no-external \ 58 | --output coverage/lcov.info 59 | genhtml \ 60 | --title 'MQTT-SN Tools' \ 61 | --output-directory coverage \ 62 | coverage/lcov.info 63 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | Version 0.0.6 (2017-08-23) 5 | -------------------------- 6 | 7 | - Added support for publishing and subscribing with QoS 1 8 | - Added support for reading from a file using -f 9 | - Added support for reading from STDIN using -s 10 | - Added support for publishing one message per line using -l 11 | - Added support for subscribing to multiple topics 12 | - Added timeout when waiting for packets 13 | - Made Keep Alive configurable in mqtt-sn-pub 14 | - Changed default keep alive for mqtt-sn-pub to 10 seconds 15 | - Fix for displaying IPv6 addresses 16 | - Improved debug error messages when connecting fails 17 | 18 | 19 | Version 0.0.5 (2015-11-04) 20 | -------------------------- 21 | 22 | - Added extensive test suite written in Ruby 23 | - Added return code strings to packet dump output 24 | - Display when connecting and disconnecting starts. 25 | - Changed error messages that aren't fatal to warnings 26 | - Added signal handling to mqtt-sn-dump 27 | - Fix to ensure that packet data structures are set to 0 before use 28 | - Added mqtt-sn-dump tool 29 | - Added debug of source IP and port when receiving a packet 30 | - Implemented setting the Clean Session flag 31 | - Fix for subscribing to short/predefined topics 32 | - New command line option -V to display current time and the topic name in verbose mode. 33 | - Added install and uninstall targets to the Makefile 34 | - Added Forwarder Encapsulation support 35 | - Added current time and log level to log messages 36 | - Wait for disconnect acknowledge into mqtt-sn-pub and mqtt-sn-sub. 37 | - Display string, not hex for unexpected packet type 38 | - Fixed incorrect pointer handling in mqtt-sn-sub 39 | 40 | 41 | Version 0.0.4 (2014-06-01) 42 | -------------------------- 43 | 44 | - Changed license to MIT 45 | - Added mqtt-sn-serial-bridge 46 | - Simplified the Makefile 47 | - Improvements to cope with multiple packets received in close succession 48 | 49 | 50 | Version 0.0.3 (2013-12-07) 51 | -------------------------- 52 | 53 | - Added BSD 2-Clause License 54 | - Renamed 'Unsupported Features' to limitations. 55 | - Added checks to make sure various strings aren’t too long 56 | - Corrected the maximum length of a MQTT-SN packet (in this tool) 57 | - Added support for displaying short and pre-defined topic types in verbose mode 58 | - 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) 59 | - Added support for displaying topic name for wildcard subscriptions (aka verbose mode) 60 | - Noted that QoS −1 is now supported in the README 61 | - Added support for QoS Level −1 (tested against RSMB) 62 | - Corrected help text for pre-defined topic ID parameter 63 | - Shortened clientid argument help text 64 | - Enabled keep-alive timer in mqtt-sn-pub - will cause sessions to be cleaned up by the broker if client is unexpectedly disconnected 65 | 66 | 67 | Version 0.0.2 (2013-12-02) 68 | -------------------------- 69 | 70 | - Renamed from MQTT-S to MQTT-SN to match specification change 71 | - Added support for publishing to short and pre-defined topic ids 72 | - Added setting of QoS flag in matts_send_publish 73 | - Renamed default client id prefix to mqtts-tools- 74 | 75 | 76 | Version 0.0.1 (2013-04-02) 77 | -------------------------- 78 | 79 | - Initial Release 80 | - Contains just mqtts-sub and mqtts-pub 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MQTT-SN Tools 2 | ============= 3 | 4 | [![Build Status](https://travis-ci.org/njh/mqtt-sn-tools.svg?branch=master)](https://travis-ci.org/njh/mqtt-sn-tools) 5 | 6 | Command line tools written in C for the MQTT-SN (MQTT for Sensor Networks) protocol. 7 | 8 | 9 | Supported Features 10 | ------------------ 11 | 12 | - QoS 0, 1 and -1 13 | - Keep alive pings 14 | - Publishing retained messages 15 | - Publishing empty messages 16 | - Subscribing to named topic 17 | - Clean / unclean sessions 18 | - Manual and automatic client ID generation 19 | - Displaying topic name with wildcard subscriptions 20 | - Pre-defined topic IDs and short topic names 21 | - Forwarder encapsulation according to MQTT-SN Protocol Specification v1.2. 22 | 23 | 24 | Limitations 25 | ----------- 26 | 27 | - Packets must be 255 or less bytes long 28 | - No Last Will and Testament 29 | - No QoS 2 30 | - No automatic re-sending of lost packets 31 | - No Automatic gateway discovery 32 | 33 | 34 | Building 35 | -------- 36 | 37 | Just run 'make' on a POSIX system. 38 | 39 | 40 | Publishing 41 | ---------- 42 | 43 | Usage: mqtt-sn-pub [opts] -t -m 44 | 45 | -d Increase debug level by one. -d can occur multiple times. 46 | -f A file to send as the message payload. 47 | -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. 48 | -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id. 49 | -k keep alive in seconds for this client. Defaults to 10. 50 | -e sleep duration in seconds when disconnecting. Defaults to 0. 51 | -m Message payload to send. 52 | -l Read from STDIN, one message per line. 53 | -n Send a null (zero length) message. 54 | -p Network port to connect to. Defaults to 1883. 55 | -q Quality of Service value (0, 1 or -1). Defaults to 0. 56 | -r Message should be retained. 57 | -s Read one whole message from STDIN. 58 | -t MQTT-SN topic name to publish to. 59 | -T Pre-defined MQTT-SN topic ID to publish to. 60 | --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. 61 | --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id. 62 | --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. 63 | 64 | 65 | Subscribing 66 | ----------- 67 | 68 | Usage: mqtt-sn-sub [opts] -t 69 | 70 | -1 exit after receiving a single message. 71 | -c disable 'clean session' (store subscription and pending messages when client disconnects). 72 | -d Increase debug level by one. -d can occur multiple times. 73 | -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. 74 | -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id. 75 | -k keep alive in seconds for this client. Defaults to 10. 76 | -e sleep duration in seconds when disconnecting. Defaults to 0. 77 | -p Network port to connect to. Defaults to 1883. 78 | -q QoS level to subscribe with (0 or 1). Defaults to 0. 79 | -t MQTT-SN topic name to subscribe to. It may repeat multiple times. 80 | -T Pre-defined MQTT-SN topic ID to subscribe to. It may repeat multiple times. 81 | --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. 82 | --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id. 83 | -v Print messages verbosely, showing the topic name. 84 | -V Print messages verbosely, showing current time and the topic name. 85 | --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. 86 | 87 | 88 | Dumping 89 | ------- 90 | 91 | Displays MQTT-SN packets sent to specified port. 92 | Most useful for listening out for QoS -1 messages being published by a client. 93 | 94 | Usage: mqtt-sn-dump [opts] -p 95 | 96 | -a Dump all packet types. 97 | -d Increase debug level by one. -d can occur multiple times. 98 | -p Network port to listen on. Defaults to 1883. 99 | -v Print messages verbosely, showing the topic name. 100 | 101 | 102 | Serial Port Bridge 103 | ------------------ 104 | 105 | The Serial Port bridge can be used to relay packets from a remote device on the end of a 106 | serial port and convert them into UDP packets, which are sent and received from a broker 107 | or MQTT-SN gateway. 108 | 109 | Usage: mqtt-sn-serial-bridge [opts] 110 | 111 | -b Set the baud rate. Defaults to 9600. 112 | -d Increase debug level by one. -d can occur multiple times. 113 | -dd Enable extended debugging - display packets in hex. 114 | -h MQTT-SN host to connect to. Defaults to '127.0.0.1'. 115 | -p Network port to connect to. Defaults to 1883. 116 | --fe Enables Forwarder Encapsulation. Mqtt-sn packets are encapsulated according to MQTT-SN Protocol Specification v1.2, chapter 5.5 Forwarder Encapsulation. 117 | --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to 0. 118 | 119 | 120 | License 121 | ------- 122 | 123 | MQTT-SN Tools is licensed under the [MIT License]. 124 | 125 | 126 | 127 | [MIT License]: http://opensource.org/licenses/MIT 128 | -------------------------------------------------------------------------------- /doc/MQTT-SN_spec_v1.2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/mqtt-sn-tools/f2ecdaa88de1aa9eaedee2f707016fe1c4341db3/doc/MQTT-SN_spec_v1.2.pdf -------------------------------------------------------------------------------- /mqtt-sn-dump.c: -------------------------------------------------------------------------------- 1 | /* 2 | MQTT-SN command-line dump 3 | Copyright (C) Nicholas Humfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #include "mqtt-sn.h" 37 | 38 | const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; 39 | uint8_t dump_all = FALSE; 40 | uint8_t debug = 0; 41 | uint8_t verbose = 0; 42 | uint8_t keep_running = TRUE; 43 | 44 | 45 | static void usage() 46 | { 47 | fprintf(stderr, "Usage: mqtt-sn-dump [opts] -p \n"); 48 | fprintf(stderr, "\n"); 49 | fprintf(stderr, " -a Dump all packet types.\n"); 50 | fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); 51 | fprintf(stderr, " -p Network port to listen on. Defaults to %s.\n", mqtt_sn_port); 52 | fprintf(stderr, " -v Print messages verbosely, showing the topic name.\n"); 53 | exit(EXIT_FAILURE); 54 | } 55 | 56 | static void parse_opts(int argc, char** argv) 57 | { 58 | int ch; 59 | 60 | // Parse the options/switches 61 | while((ch = getopt(argc, argv, "adp:v?")) != -1) 62 | switch(ch) { 63 | case 'a': 64 | dump_all = TRUE; 65 | break; 66 | 67 | case 'd': 68 | debug++; 69 | break; 70 | 71 | case 'p': 72 | mqtt_sn_port = optarg; 73 | break; 74 | 75 | case 'v': 76 | verbose++; 77 | break; 78 | 79 | case '?': 80 | default: 81 | usage(); 82 | break; 83 | } 84 | } 85 | 86 | static int bind_udp_socket(const char* port_str) 87 | { 88 | struct sockaddr_in si_me; 89 | short port = atoi(port_str); 90 | int sock; 91 | 92 | if ((sock=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) { 93 | perror("socket"); 94 | exit(EXIT_FAILURE); 95 | } 96 | 97 | memset((char *) &si_me, 0, sizeof(si_me)); 98 | si_me.sin_family = AF_INET; 99 | si_me.sin_port = htons(port); 100 | si_me.sin_addr.s_addr = htonl(INADDR_ANY); 101 | if (bind(sock, (const struct sockaddr *)&si_me, sizeof(si_me)) == -1) { 102 | perror("bind"); 103 | exit(EXIT_FAILURE); 104 | } 105 | 106 | mqtt_sn_log_debug("mqtt-sn-dump listening on port %s", port_str); 107 | 108 | return sock; 109 | } 110 | 111 | static void termination_handler (int signum) 112 | { 113 | switch(signum) { 114 | case SIGHUP: 115 | mqtt_sn_log_debug("Got hangup signal."); 116 | break; 117 | case SIGTERM: 118 | mqtt_sn_log_debug("Got termination signal."); 119 | break; 120 | case SIGINT: 121 | mqtt_sn_log_debug("Got interrupt signal."); 122 | break; 123 | } 124 | 125 | // Signal the main thread to stop 126 | keep_running = FALSE; 127 | } 128 | 129 | int main(int argc, char* argv[]) 130 | { 131 | int sock; 132 | 133 | // Disable buffering on STDOUT 134 | setvbuf(stdout, NULL, _IONBF, 0); 135 | 136 | // Parse the command-line options 137 | parse_opts(argc, argv); 138 | 139 | // Enable debugging? 140 | mqtt_sn_set_debug(debug); 141 | mqtt_sn_set_verbose(verbose); 142 | 143 | // Setup signal handlers 144 | signal(SIGTERM, termination_handler); 145 | signal(SIGINT, termination_handler); 146 | signal(SIGHUP, termination_handler); 147 | 148 | // Create a listening UDP socket 149 | sock = bind_udp_socket(mqtt_sn_port); 150 | 151 | while (keep_running) { 152 | int ret = mqtt_sn_select(sock); 153 | if (ret < 0) { 154 | break; 155 | } else if (ret > 0) { 156 | char* packet = mqtt_sn_receive_packet(sock); 157 | if (dump_all) { 158 | mqtt_sn_dump_packet(packet); 159 | } else if (packet[1] == MQTT_SN_TYPE_PUBLISH) { 160 | mqtt_sn_print_publish_packet((publish_packet_t *)packet); 161 | } 162 | } 163 | } 164 | 165 | close(sock); 166 | 167 | return 0; 168 | } 169 | 170 | -------------------------------------------------------------------------------- /mqtt-sn-pub.c: -------------------------------------------------------------------------------- 1 | /* 2 | MQTT-SN command-line publishing client 3 | Copyright (C) Nicholas Humfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include "mqtt-sn.h" 38 | 39 | const char *client_id = NULL; 40 | const char *topic_name = NULL; 41 | const char *message_data = NULL; 42 | const char *message_file = NULL; 43 | const char *mqtt_sn_host = "127.0.0.1"; 44 | const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; 45 | uint16_t source_port = 0; 46 | uint16_t topic_id = 0; 47 | uint8_t topic_id_type = MQTT_SN_TOPIC_TYPE_NORMAL; 48 | uint16_t keep_alive = MQTT_SN_DEFAULT_KEEP_ALIVE; 49 | uint16_t sleep_duration = 0; 50 | int8_t qos = 0; 51 | uint8_t retain = FALSE; 52 | uint8_t one_message_per_line = FALSE; 53 | uint8_t debug = 0; 54 | 55 | 56 | static void usage() 57 | { 58 | fprintf(stderr, "Usage: mqtt-sn-pub [opts] -t -m \n"); 59 | fprintf(stderr, "\n"); 60 | fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); 61 | fprintf(stderr, " -f A file to send as the message payload.\n"); 62 | fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); 63 | fprintf(stderr, " -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id.\n"); 64 | fprintf(stderr, " -k keep alive in seconds for this client. Defaults to %d.\n", keep_alive); 65 | fprintf(stderr, " -e sleep duration in seconds when disconnecting. Defaults to %d.\n", sleep_duration); 66 | fprintf(stderr, " -m Message payload to send.\n"); 67 | fprintf(stderr, " -l Read from STDIN, one message per line.\n"); 68 | fprintf(stderr, " -n Send a null (zero length) message.\n"); 69 | fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); 70 | fprintf(stderr, " -q Quality of Service value (0, 1 or -1). Defaults to %d.\n", qos); 71 | fprintf(stderr, " -r Message should be retained.\n"); 72 | fprintf(stderr, " -s Read one whole message from STDIN.\n"); 73 | fprintf(stderr, " -t MQTT-SN topic name to publish to.\n"); 74 | fprintf(stderr, " -T Pre-defined MQTT-SN topic ID to publish to.\n"); 75 | 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"); 76 | fprintf(stderr, " --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id.\n"); 77 | fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); 78 | exit(EXIT_FAILURE); 79 | } 80 | 81 | static void parse_opts(int argc, char** argv) 82 | { 83 | 84 | static struct option long_options[] = { 85 | {"fe", no_argument, 0, 1000 }, 86 | {"wlnid", required_argument, 0, 1001 }, 87 | {"cport", required_argument, 0, 1002 }, 88 | {0, 0, 0, 0} 89 | }; 90 | 91 | int ch; 92 | /* getopt_long stores the option index here. */ 93 | int option_index = 0; 94 | 95 | // Parse the options/switches 96 | while ((ch = getopt_long (argc, argv, "df:h:i:k:e:lm:np:q:rst:T:?", long_options, &option_index)) != -1) { 97 | switch (ch) { 98 | case 'd': 99 | debug++; 100 | break; 101 | 102 | case 'h': 103 | mqtt_sn_host = optarg; 104 | break; 105 | 106 | case 'f': 107 | message_file = optarg; 108 | break; 109 | 110 | case 'i': 111 | client_id = optarg; 112 | break; 113 | 114 | case 'l': 115 | message_file = "-"; 116 | one_message_per_line = TRUE; 117 | break; 118 | 119 | case 'm': 120 | message_data = optarg; 121 | break; 122 | 123 | case 'k': 124 | keep_alive = atoi(optarg); 125 | break; 126 | 127 | case 'e': 128 | sleep_duration = atoi(optarg); 129 | break; 130 | 131 | case 'n': 132 | message_data = ""; 133 | break; 134 | 135 | case 'p': 136 | mqtt_sn_port = optarg; 137 | break; 138 | 139 | case 'q': 140 | qos = atoi(optarg); 141 | break; 142 | 143 | case 'r': 144 | retain = TRUE; 145 | break; 146 | 147 | case 's': 148 | message_file = "-"; 149 | one_message_per_line = FALSE; 150 | break; 151 | 152 | case 't': 153 | topic_name = optarg; 154 | break; 155 | 156 | case 'T': 157 | topic_id = atoi(optarg); 158 | break; 159 | 160 | case 1000: 161 | mqtt_sn_enable_frwdencap(); 162 | break; 163 | 164 | case 1001: 165 | mqtt_sn_set_frwdencap_parameters((uint8_t*)optarg, strlen(optarg)); 166 | break; 167 | 168 | case 1002: 169 | source_port = atoi(optarg); 170 | break; 171 | 172 | case '?': 173 | default: 174 | usage(); 175 | break; 176 | } // switch 177 | } // while 178 | 179 | // Missing Parameter? 180 | if (!(topic_name || topic_id) || !(message_data || message_file)) { 181 | usage(); 182 | } 183 | 184 | if (qos != -1 && qos != 0 && qos != 1) { 185 | mqtt_sn_log_err("Only QoS level 0, 1 or -1 is supported."); 186 | exit(EXIT_FAILURE); 187 | } 188 | 189 | // Both topic name and topic id? 190 | if (topic_name && topic_id) { 191 | mqtt_sn_log_err("Please provide either a topic id or a topic name, not both."); 192 | exit(EXIT_FAILURE); 193 | } 194 | 195 | // Both message data and file? 196 | if (message_data && message_file) { 197 | mqtt_sn_log_err("Please provide either message data or a message file, not both."); 198 | exit(EXIT_FAILURE); 199 | } 200 | 201 | // Check topic is valid for QoS level -1 202 | if (qos == -1 && topic_id == 0 && strlen(topic_name) != 2) { 203 | mqtt_sn_log_err("Either a pre-defined topic id or a short topic name must be given for QoS -1."); 204 | exit(EXIT_FAILURE); 205 | } 206 | } 207 | 208 | static void publish_file(int sock, const char* filename) 209 | { 210 | char buffer[MQTT_SN_MAX_PAYLOAD_LENGTH]; 211 | uint16_t message_len = 0; 212 | FILE* file = NULL; 213 | 214 | if (strcmp(filename, "-") == 0) { 215 | file = stdin; 216 | } else { 217 | file = fopen(filename, "rb"); 218 | } 219 | 220 | if (!file) { 221 | perror("Failed to open message file"); 222 | exit(EXIT_FAILURE); 223 | } 224 | 225 | if (one_message_per_line) { 226 | // One message per line 227 | while(!feof(file) && !ferror(file)) { 228 | char* message = fgets(buffer, MQTT_SN_MAX_PAYLOAD_LENGTH, file); 229 | if (message) { 230 | char* end = strpbrk(message, "\n\r"); 231 | if (end) { 232 | uint16_t message_len = (end - message); 233 | mqtt_sn_send_publish(sock, topic_id, topic_id_type, message, message_len, qos, retain); 234 | } else { 235 | mqtt_sn_log_err("Failed to find newline when reading message"); 236 | } 237 | } else if (ferror(file)) { 238 | perror("Failed to read message line"); 239 | exit(EXIT_FAILURE); 240 | } 241 | } 242 | } else { 243 | // One message until EOF 244 | message_len = fread(buffer, 1, MQTT_SN_MAX_PAYLOAD_LENGTH, file); 245 | if (ferror(file)) { 246 | perror("Failed to read message file"); 247 | exit(EXIT_FAILURE); 248 | } else if (!feof(file)) { 249 | mqtt_sn_log_warn("Input file is longer than the maximum message size"); 250 | } 251 | 252 | mqtt_sn_send_publish(sock, topic_id, topic_id_type, buffer, message_len, qos, retain); 253 | } 254 | 255 | fclose(file); 256 | } 257 | 258 | int main(int argc, char* argv[]) 259 | { 260 | int sock; 261 | 262 | mqtt_sn_disable_frwdencap(); 263 | 264 | // Parse the command-line options 265 | parse_opts(argc, argv); 266 | 267 | // Enable debugging? 268 | mqtt_sn_set_debug(debug); 269 | mqtt_sn_set_timeout(keep_alive / 2); 270 | 271 | // Create a UDP socket 272 | sock = mqtt_sn_create_socket(mqtt_sn_host, mqtt_sn_port, source_port); 273 | if (sock) { 274 | // Connect to gateway 275 | if (qos >= 0) { 276 | mqtt_sn_log_debug("Connecting..."); 277 | mqtt_sn_send_connect(sock, client_id, keep_alive, TRUE); 278 | mqtt_sn_receive_connack(sock); 279 | } 280 | 281 | if (topic_id) { 282 | // Use pre-defined topic ID 283 | topic_id_type = MQTT_SN_TOPIC_TYPE_PREDEFINED; 284 | } else if (strlen(topic_name) == 2) { 285 | // Convert the 2 character topic name into a 2 byte topic id 286 | topic_id = (topic_name[0] << 8) + topic_name[1]; 287 | topic_id_type = MQTT_SN_TOPIC_TYPE_SHORT; 288 | } else if (qos >= 0) { 289 | // Register the topic name 290 | mqtt_sn_send_register(sock, topic_name); 291 | topic_id = mqtt_sn_receive_regack(sock); 292 | topic_id_type = MQTT_SN_TOPIC_TYPE_NORMAL; 293 | } 294 | 295 | // Publish to the topic 296 | if (message_file) { 297 | publish_file(sock, message_file); 298 | } else { 299 | uint16_t message_len = strlen(message_data); 300 | mqtt_sn_send_publish(sock, topic_id, topic_id_type, message_data, message_len, qos, retain); 301 | } 302 | 303 | // Finally, disconnect 304 | if (qos >= 0) { 305 | mqtt_sn_log_debug("Disconnecting..."); 306 | mqtt_sn_send_disconnect(sock, sleep_duration); 307 | mqtt_sn_receive_disconnect(sock); 308 | } 309 | 310 | close(sock); 311 | } 312 | 313 | mqtt_sn_cleanup(); 314 | 315 | return 0; 316 | } 317 | -------------------------------------------------------------------------------- /mqtt-sn-serial-bridge.c: -------------------------------------------------------------------------------- 1 | /* 2 | MQTT-SN serial bridge 3 | Copyright (C) Nicholas Humfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | //#define _POSIX_SOURCE 1 /* POSIX compliant source */ 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #include "mqtt-sn.h" 45 | 46 | 47 | const char *mqtt_sn_host = "127.0.0.1"; 48 | const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; 49 | const char *serial_device = NULL; 50 | uint16_t source_port = 0; 51 | int serial_baud = 9600; 52 | uint8_t debug = 0; 53 | uint8_t frwdencap = FALSE; 54 | 55 | uint8_t keep_running = TRUE; 56 | 57 | static speed_t baud_lookup(int baud) 58 | { 59 | switch(baud) { 60 | case 0: 61 | return B0; 62 | case 50: 63 | return B50; 64 | case 75: 65 | return B75; 66 | case 110: 67 | return B110; 68 | case 134: 69 | return B134; 70 | case 150: 71 | return B150; 72 | case 200: 73 | return B200; 74 | case 300: 75 | return B300; 76 | case 600: 77 | return B600; 78 | case 1200: 79 | return B1200; 80 | case 1800: 81 | return B1800; 82 | case 2400: 83 | return B2400; 84 | case 4800: 85 | return B4800; 86 | case 9600: 87 | return B9600; 88 | case 19200: 89 | return B19200; 90 | case 38400: 91 | return B38400; 92 | case 57600: 93 | return B57600; 94 | case 115200: 95 | return B115200; 96 | case 230400: 97 | return B230400; 98 | default: 99 | fprintf(stderr, "Unsupported baud rate: %d\n", baud); 100 | exit(1); 101 | } 102 | } 103 | 104 | static void usage() 105 | { 106 | fprintf(stderr, "Usage: mqtt-sn-serial-bridge [opts] \n"); 107 | fprintf(stderr, "\n"); 108 | fprintf(stderr, " -b Set the baud rate. Defaults to %d.\n", (int)serial_baud); 109 | fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); 110 | fprintf(stderr, " -dd Enable extended debugging - display packets in hex.\n"); 111 | fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); 112 | fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); 113 | 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"); 114 | fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); 115 | exit(EXIT_FAILURE); 116 | } 117 | 118 | static void parse_opts(int argc, char** argv) 119 | { 120 | 121 | static struct option long_options[] = { 122 | {"fe", no_argument, 0, 1000 }, 123 | {"cport", required_argument, 0, 1001 }, 124 | {0, 0, 0, 0} 125 | }; 126 | 127 | int ch; 128 | /* getopt_long stores the option index here. */ 129 | int option_index = 0; 130 | 131 | // Parse the options/switches 132 | while ((ch = getopt_long (argc, argv, "b:dh:p:?", long_options, &option_index)) != -1) { 133 | switch (ch) { 134 | case 'b': 135 | serial_baud = atoi(optarg); 136 | baud_lookup(serial_baud); 137 | break; 138 | 139 | case 'd': 140 | debug++; 141 | break; 142 | 143 | case 'h': 144 | mqtt_sn_host = optarg; 145 | break; 146 | 147 | case 'p': 148 | mqtt_sn_port = optarg; 149 | break; 150 | 151 | case 1000: 152 | mqtt_sn_enable_frwdencap(); 153 | frwdencap = TRUE; 154 | break; 155 | case 1001: 156 | source_port = atoi(optarg); 157 | break; 158 | 159 | case '?': 160 | default: 161 | usage(); 162 | break; 163 | } // switch 164 | } // while 165 | 166 | // Final argument is the serial port device path 167 | if (argc-optind < 1) { 168 | fprintf(stderr, "Missing serial port.\n"); 169 | usage(); 170 | } else { 171 | serial_device = argv[optind]; 172 | } 173 | } 174 | 175 | 176 | static int serial_open(const char* device_path) 177 | { 178 | struct termios tios; 179 | int fd; 180 | 181 | mqtt_sn_disable_frwdencap(); 182 | 183 | mqtt_sn_log_debug("Opening %s", device_path); 184 | 185 | fd = open(device_path, O_RDWR | O_NOCTTY | O_NDELAY); 186 | if (fd < 0) { 187 | perror(device_path); 188 | exit(EXIT_FAILURE); 189 | } 190 | 191 | // Turn back on blocking reads 192 | fcntl(fd, F_SETFL, 0); 193 | 194 | // Read existing serial port settings 195 | tcgetattr(fd, &tios); 196 | 197 | // Set the input and output baud rates 198 | cfsetispeed(&tios, baud_lookup(serial_baud)); 199 | cfsetospeed(&tios, baud_lookup(serial_baud)); 200 | 201 | // Set to local mode 202 | tios.c_cflag |= CLOCAL | CREAD; 203 | 204 | // Set serial port mode to 8N1 205 | tios.c_cflag &= ~PARENB; 206 | tios.c_cflag &= ~CSTOPB; 207 | tios.c_cflag &= ~CSIZE; 208 | tios.c_cflag |= CS8; 209 | 210 | // Turn off flow control and ignore parity 211 | tios.c_iflag &= ~(IXON | IXOFF | IXANY); 212 | tios.c_iflag |= IGNPAR; 213 | 214 | // Turn off output post-processing 215 | tios.c_oflag &= ~OPOST; 216 | 217 | // set input mode (non-canonical, no echo,...) 218 | tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); 219 | 220 | // Set VMIN high, so that we wait for a gap in the data 221 | // http://www.unixwiz.net/techtips/termios-vmin-vtime.html 222 | tios.c_cc[VMIN] = 255; 223 | tios.c_cc[VTIME] = 1; 224 | 225 | tcsetattr(fd, TCSAFLUSH, &tios); 226 | 227 | // Flush the input buffer 228 | sleep(1); 229 | tcflush(fd, TCIOFLUSH); 230 | 231 | return fd; 232 | } 233 | 234 | static void* serial_read_packet(int fd) 235 | { 236 | static uint8_t buf[MQTT_SN_MAX_PACKET_LENGTH+1]; 237 | 238 | // First get the length of the packet 239 | int bytes_read = read(fd, buf, 1); 240 | if (bytes_read != 1) { 241 | mqtt_sn_log_err("Error reading packet length from serial port: %d, %d", bytes_read, errno); 242 | return NULL; 243 | } 244 | 245 | if (buf[0] == 0x00) { 246 | mqtt_sn_log_err("Packets of length 0 are invalid."); 247 | return NULL; 248 | } 249 | 250 | // Read in the rest of the packet 251 | bytes_read = read(fd, &buf[1], buf[0]-1); 252 | if (bytes_read <= 0) { 253 | mqtt_sn_log_err("Error reading rest of packet from serial port: %d, %d", bytes_read, errno); 254 | return NULL; 255 | } else { 256 | bytes_read += 1; 257 | } 258 | 259 | if (mqtt_sn_validate_packet(buf, bytes_read) == FALSE) { 260 | return NULL; 261 | } 262 | 263 | // NULL-terminate the packet 264 | buf[bytes_read] = '\0'; 265 | 266 | if (debug) { 267 | const char* type = mqtt_sn_type_string(buf[1]); 268 | mqtt_sn_log_debug("Serial -> UDP (bytes_read=%d, type=%s)", bytes_read, type); 269 | if (debug > 1) { 270 | int i; 271 | fprintf(stderr, " "); 272 | for (i=0; i 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #include "mqtt-sn.h" 39 | 40 | const char *client_id = NULL; 41 | // Array of pointers to topic names 42 | char **topic_name_ar = NULL; 43 | // Topic names array size. When too small it is incremented by ARRAY_INCREMENT. 44 | uint16_t topic_name_ar_size = 0; 45 | uint16_t topic_name_index = 0; 46 | // Array of predefined topic IDs to subscribe to 47 | uint16_t *predef_topic_id_ar = NULL; 48 | // Predefined topic IDs array size. When too small it is incremented by ARRAY_INCREMENT. 49 | uint16_t predef_topic_id_ar_size = 0; 50 | uint16_t predef_topic_id_index = 0; 51 | #define ARRAY_INCREMENT 10 52 | const char *mqtt_sn_host = "127.0.0.1"; 53 | const char *mqtt_sn_port = MQTT_SN_DEFAULT_PORT; 54 | uint16_t source_port = 0; 55 | uint16_t keep_alive = MQTT_SN_DEFAULT_KEEP_ALIVE; 56 | uint16_t sleep_duration = 0; 57 | int8_t qos = 0; 58 | uint8_t retain = FALSE; 59 | uint8_t debug = 0; 60 | uint8_t single_message = FALSE; 61 | uint8_t clean_session = TRUE; 62 | uint8_t verbose = 0; 63 | 64 | uint8_t keep_running = TRUE; 65 | 66 | static void usage() 67 | { 68 | fprintf(stderr, "Usage: mqtt-sn-sub [opts] -t \n"); 69 | fprintf(stderr, "\n"); 70 | fprintf(stderr, " -1 exit after receiving a single message.\n"); 71 | fprintf(stderr, " -c disable 'clean session' (store subscription and pending messages when client disconnects).\n"); 72 | fprintf(stderr, " -d Increase debug level by one. -d can occur multiple times.\n"); 73 | fprintf(stderr, " -h MQTT-SN host to connect to. Defaults to '%s'.\n", mqtt_sn_host); 74 | fprintf(stderr, " -i ID to use for this client. Defaults to 'mqtt-sn-tools-' with process id.\n"); 75 | fprintf(stderr, " -k keep alive in seconds for this client. Defaults to %d.\n", keep_alive); 76 | fprintf(stderr, " -e sleep duration in seconds when disconnecting. Defaults to %d.\n", sleep_duration); 77 | fprintf(stderr, " -p Network port to connect to. Defaults to %s.\n", mqtt_sn_port); 78 | fprintf(stderr, " -q QoS level to subscribe with (0 or 1). Defaults to %d.\n", qos); 79 | fprintf(stderr, " -t MQTT-SN topic name to subscribe to. It may repeat multiple times.\n"); 80 | fprintf(stderr, " -T Pre-defined MQTT-SN topic ID to subscribe to. It may repeat multiple times.\n"); 81 | 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"); 82 | fprintf(stderr, " --wlnid If Forwarder Encapsulation is enabled, wireless node ID for this client. Defaults to process id.\n"); 83 | fprintf(stderr, " --cport Source port for outgoing packets. Uses port in ephemeral range if not specified or set to %d.\n", source_port); 84 | fprintf(stderr, " -v Print messages verbosely, showing the topic name.\n"); 85 | fprintf(stderr, " -V Print messages verbosely, showing current time and the topic name.\n"); 86 | exit(EXIT_FAILURE); 87 | } 88 | 89 | static void parse_opts(int argc, char** argv) 90 | { 91 | 92 | static struct option long_options[] = { 93 | {"fe", no_argument, 0, 1000 }, 94 | {"wlnid", required_argument, 0, 1001 }, 95 | {"cport", required_argument, 0, 1002 }, 96 | {0, 0, 0, 0} 97 | }; 98 | 99 | int ch; 100 | /* getopt_long stores the option index here. */ 101 | int option_index = 0; 102 | 103 | // Parse the options/switches 104 | while ((ch = getopt_long(argc, argv, "1cdh:i:k:e:p:q:t:T:vV?", long_options, &option_index)) != -1) 105 | switch (ch) { 106 | case '1': 107 | single_message = TRUE; 108 | break; 109 | 110 | case 'c': 111 | clean_session = FALSE; 112 | break; 113 | 114 | case 'd': 115 | debug++; 116 | break; 117 | 118 | case 'h': 119 | mqtt_sn_host = optarg; 120 | break; 121 | 122 | case 'i': 123 | client_id = optarg; 124 | break; 125 | 126 | case 'k': 127 | keep_alive = atoi(optarg); 128 | break; 129 | 130 | case 'e': 131 | sleep_duration = atoi(optarg); 132 | break; 133 | 134 | case 'p': 135 | mqtt_sn_port = optarg; 136 | break; 137 | 138 | case 'q': 139 | qos = atoi(optarg); 140 | break; 141 | 142 | case 't': 143 | // Resize topic names array when too small 144 | if (topic_name_index == topic_name_ar_size) { 145 | topic_name_ar = realloc(topic_name_ar, (topic_name_ar_size + ARRAY_INCREMENT) * sizeof(char*)) ; 146 | topic_name_ar_size += ARRAY_INCREMENT; 147 | } 148 | topic_name_ar[topic_name_index++] = optarg; 149 | break; 150 | 151 | case 'T': 152 | // Resize predefined topic IDs array when too small 153 | if (predef_topic_id_index == predef_topic_id_ar_size) { 154 | predef_topic_id_ar = realloc(predef_topic_id_ar, (predef_topic_id_ar_size + ARRAY_INCREMENT) * sizeof(uint16_t)); 155 | predef_topic_id_ar_size += ARRAY_INCREMENT; 156 | } 157 | predef_topic_id_ar[predef_topic_id_index++] = atoi(optarg); 158 | break; 159 | 160 | case 1000: 161 | mqtt_sn_enable_frwdencap(); 162 | break; 163 | 164 | case 1001: 165 | mqtt_sn_set_frwdencap_parameters((uint8_t*)optarg, strlen(optarg)); 166 | break; 167 | 168 | case 1002: 169 | source_port = atoi(optarg); 170 | break; 171 | 172 | case 'v': 173 | // Prevent -v setting verbose level back down to 1 if already set to 2 by -V 174 | verbose = (verbose == 0) ? 1 : verbose; 175 | break; 176 | 177 | case 'V': 178 | verbose = 2; 179 | break; 180 | 181 | case '?': 182 | default: 183 | usage(); 184 | break; 185 | } 186 | 187 | // Missing Parameter? 188 | if (!topic_name_index && !predef_topic_id_index) { 189 | usage(); 190 | } 191 | } 192 | 193 | static void termination_handler (int signum) 194 | { 195 | switch(signum) { 196 | case SIGHUP: 197 | mqtt_sn_log_debug("Got hangup signal."); 198 | break; 199 | case SIGTERM: 200 | mqtt_sn_log_debug("Got termination signal."); 201 | break; 202 | case SIGINT: 203 | mqtt_sn_log_debug("Got interrupt signal."); 204 | break; 205 | } 206 | 207 | // Signal the main thread to stop 208 | keep_running = FALSE; 209 | } 210 | 211 | int main(int argc, char* argv[]) 212 | { 213 | int sock; 214 | 215 | mqtt_sn_disable_frwdencap(); 216 | 217 | // Parse the command-line options 218 | parse_opts(argc, argv); 219 | 220 | // Enable debugging? 221 | mqtt_sn_set_debug(debug); 222 | mqtt_sn_set_verbose(verbose); 223 | mqtt_sn_set_timeout(keep_alive / 2); 224 | 225 | // Setup signal handlers 226 | signal(SIGTERM, termination_handler); 227 | signal(SIGINT, termination_handler); 228 | signal(SIGHUP, termination_handler); 229 | 230 | // Create a UDP socket 231 | sock = mqtt_sn_create_socket(mqtt_sn_host, mqtt_sn_port, source_port); 232 | if (sock) { 233 | // Connect to server 234 | mqtt_sn_log_debug("Connecting..."); 235 | mqtt_sn_send_connect(sock, client_id, keep_alive, clean_session); 236 | mqtt_sn_receive_connack(sock); 237 | 238 | uint16_t i; 239 | // Subscribe to the each topic name 240 | for (i = 0; i < topic_name_index; i++) { 241 | mqtt_sn_log_debug("Subscribing to topic name: %s ...", topic_name_ar[i]); 242 | mqtt_sn_send_subscribe_topic_name(sock, topic_name_ar[i], qos); 243 | 244 | // Wait for the subscription acknowledgment 245 | uint16_t topic_id = mqtt_sn_receive_suback(sock); 246 | if (topic_id && strlen(topic_name_ar[i]) > 2) { 247 | mqtt_sn_register_topic(topic_id, topic_name_ar[i]); 248 | } 249 | } 250 | 251 | // Subscribe to the each predefined topic ID 252 | for (i = 0; i < predef_topic_id_index; i++) { 253 | mqtt_sn_log_debug("Subscribing to predefined topic ID: %u ...", predef_topic_id_ar[i]); 254 | mqtt_sn_send_subscribe_topic_id(sock, predef_topic_id_ar[i], qos); 255 | 256 | // Wait for the subscription acknowledgment 257 | mqtt_sn_receive_suback(sock); 258 | } 259 | 260 | // Keep processing packets until process is terminated 261 | while(keep_running) { 262 | publish_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_PUBLISH, sock); 263 | if (packet) { 264 | uint8_t packet_qos = packet->flags & MQTT_SN_FLAG_QOS_MASK; 265 | if (packet_qos == MQTT_SN_FLAG_QOS_1) { 266 | mqtt_sn_send_puback(sock, packet, MQTT_SN_ACCEPTED); 267 | } 268 | 269 | if (single_message) { 270 | break; 271 | } 272 | } 273 | } 274 | 275 | // Finally, disconnect 276 | mqtt_sn_log_debug("Disconnecting..."); 277 | mqtt_sn_send_disconnect(sock, sleep_duration); 278 | mqtt_sn_receive_disconnect(sock); 279 | 280 | close(sock); 281 | } 282 | 283 | mqtt_sn_cleanup(); 284 | free(topic_name_ar); 285 | free(predef_topic_id_ar); 286 | 287 | return 0; 288 | } 289 | -------------------------------------------------------------------------------- /mqtt-sn.c: -------------------------------------------------------------------------------- 1 | /* 2 | Common functions used by the MQTT-SN Tools 3 | Copyright (C) Nicholas Humfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #include "mqtt-sn.h" 40 | 41 | 42 | #ifndef AI_DEFAULT 43 | #define AI_DEFAULT (AI_ADDRCONFIG|AI_V4MAPPED) 44 | #endif 45 | 46 | static uint8_t debug = 0; 47 | static uint8_t verbose = 0; 48 | static uint8_t timeout = MQTT_SN_DEFAULT_TIMEOUT; 49 | static uint16_t next_message_id = 1; 50 | static time_t last_transmit = 0; 51 | static time_t last_receive = 0; 52 | static time_t keep_alive = 0; 53 | static uint8_t forwarder_encapsulation = FALSE; 54 | const uint8_t *wireless_node_id = NULL; 55 | uint8_t wireless_node_id_len = 0; 56 | 57 | topic_map_t *topic_map = NULL; 58 | 59 | 60 | void mqtt_sn_set_debug(uint8_t value) 61 | { 62 | debug = value; 63 | mqtt_sn_log_debug("Debug level is: %d.", debug); 64 | } 65 | 66 | void mqtt_sn_set_verbose(uint8_t value) 67 | { 68 | verbose = value; 69 | mqtt_sn_log_debug("Verbose level is: %d.", verbose); 70 | } 71 | 72 | void mqtt_sn_set_timeout(uint8_t value) 73 | { 74 | if (value < 1) { 75 | timeout = MQTT_SN_DEFAULT_TIMEOUT; 76 | } else { 77 | timeout = value; 78 | } 79 | mqtt_sn_log_debug("Network timeout is: %d seconds.", timeout); 80 | } 81 | 82 | int mqtt_sn_create_socket(const char* host, const char* port, uint16_t source_port) 83 | { 84 | struct addrinfo hints; 85 | struct addrinfo *result, *rp; 86 | struct timeval tv; 87 | int fd, ret; 88 | 89 | // Set options for the resolver 90 | memset(&hints, 0, sizeof(struct addrinfo)); 91 | hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ 92 | hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */ 93 | hints.ai_flags = 0; 94 | hints.ai_protocol = 0; /* Any protocol */ 95 | hints.ai_canonname = NULL; 96 | hints.ai_addr = NULL; 97 | hints.ai_next = NULL; 98 | 99 | // Lookup address 100 | ret = getaddrinfo(host, port, &hints, &result); 101 | if (ret != 0) { 102 | mqtt_sn_log_err("getaddrinfo: %s", gai_strerror(ret)); 103 | exit(EXIT_FAILURE); 104 | } 105 | 106 | /* getaddrinfo() returns a list of address structures. 107 | Try each address until we successfully connect(2). 108 | If socket(2) (or connect(2)) fails, we (close the socket and) 109 | try the next address. */ 110 | for (rp = result; rp != NULL; rp = rp->ai_next) { 111 | char hoststr[NI_MAXHOST] = ""; 112 | int error = 0; 113 | 114 | // Display the IP address in debug mode 115 | error = getnameinfo(rp->ai_addr, rp->ai_addrlen, 116 | hoststr, sizeof(hoststr), NULL, 0, 117 | NI_NUMERICHOST | NI_NUMERICSERV); 118 | if (error == 0) { 119 | mqtt_sn_log_debug("Trying %s...", hoststr); 120 | } else { 121 | mqtt_sn_log_debug("getnameinfo: %s", gai_strerror(ret)); 122 | } 123 | 124 | // Create a socket 125 | fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); 126 | if (fd == -1) { 127 | mqtt_sn_log_debug("Failed to create socket: %s", strerror(errno)); 128 | continue; 129 | } 130 | 131 | if (source_port != 0) { 132 | // Bind socket to the correct port 133 | struct sockaddr_in addr; 134 | memset(&addr, 0, sizeof(addr)); 135 | addr.sin_family = rp->ai_family; 136 | addr.sin_addr.s_addr = htonl(INADDR_ANY); 137 | addr.sin_port = htons(source_port); 138 | if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { 139 | mqtt_sn_log_debug("Failed to bind socket: %s", strerror(errno)); 140 | continue; 141 | } 142 | } 143 | 144 | // Connect socket to the remote host 145 | if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { 146 | // Success 147 | break; 148 | } else { 149 | mqtt_sn_log_debug("Connect failed: %s", strerror(errno)); 150 | } 151 | 152 | close(fd); 153 | } 154 | 155 | if (rp == NULL) { 156 | mqtt_sn_log_err("Could not connect to remote host."); 157 | exit(EXIT_FAILURE); 158 | } 159 | 160 | freeaddrinfo(result); 161 | 162 | // FIXME: set the Don't Fragment flag 163 | 164 | // Setup timeout on the socket 165 | tv.tv_sec = timeout; 166 | tv.tv_usec = 0; 167 | if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { 168 | perror("Error setting timeout on socket"); 169 | } 170 | 171 | return fd; 172 | } 173 | 174 | void mqtt_sn_send_packet(int sock, const void* data) 175 | { 176 | ssize_t sent = 0; 177 | size_t len = ((uint8_t*)data)[0]; 178 | 179 | // If forwarder encapsulation enabled, wrap packet 180 | if (forwarder_encapsulation) { 181 | return mqtt_sn_send_frwdencap_packet(sock, data, wireless_node_id, wireless_node_id_len); 182 | } 183 | 184 | if (debug > 1) { 185 | mqtt_sn_log_debug("Sending %2lu bytes. Type=%s on Socket: %d.", (long unsigned int)len, 186 | mqtt_sn_type_string(((uint8_t*)data)[1]), sock); 187 | } 188 | 189 | sent = send(sock, data, len, 0); 190 | if (sent != len) { 191 | mqtt_sn_log_warn("Only sent %d of %d bytes", (int)sent, (int)len); 192 | } 193 | 194 | // Store the last time that we sent a packet 195 | last_transmit = time(NULL); 196 | } 197 | 198 | void mqtt_sn_send_frwdencap_packet(int sock, const void* data, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len) 199 | { 200 | ssize_t sent = 0; 201 | size_t len = ((uint8_t*)data)[0]; 202 | uint8_t orig_packet_type = ((uint8_t*)data)[1]; 203 | frwdencap_packet_t *packet; 204 | 205 | packet = mqtt_sn_create_frwdencap_packet(data, &len, wireless_node_id, wireless_node_id_len); 206 | 207 | if (debug > 1) { 208 | mqtt_sn_log_debug("Sending %2lu bytes. Type=%s with %s inside on Socket: %d.", (long unsigned int)len, 209 | mqtt_sn_type_string(packet->type), mqtt_sn_type_string(orig_packet_type), sock); 210 | } 211 | 212 | sent = send(sock, packet, len, 0); 213 | if (sent != len) { 214 | mqtt_sn_log_debug("Warning: only sent %d of %d bytes.", (int)sent, (int)len); 215 | } 216 | 217 | // Store the last time that we sent a packet 218 | last_transmit = time(NULL); 219 | 220 | free(packet); 221 | } 222 | 223 | uint8_t mqtt_sn_validate_packet(const void *packet, size_t length) 224 | { 225 | const uint8_t* buf = packet; 226 | 227 | if (buf[0] == 0x00) { 228 | mqtt_sn_log_warn("Packet length header is not valid"); 229 | return FALSE; 230 | } 231 | 232 | if (buf[0] == 0x01) { 233 | mqtt_sn_log_warn("Packet received is longer than this tool can handle"); 234 | return FALSE; 235 | } 236 | 237 | // When forwarder encapsulation is enabled each packet must be FRWDENCAP type 238 | if (forwarder_encapsulation && buf[1] != MQTT_SN_TYPE_FRWDENCAP) { 239 | mqtt_sn_log_warn("Expecting FRWDENCAP packet and got Type=%s.", mqtt_sn_type_string(buf[1])); 240 | return FALSE; 241 | } 242 | 243 | // If packet is forwarder encapsulation expected packet length is sum of forwarder encapsulation 244 | // header and length of encapsulated packet. 245 | if ((buf[1] == MQTT_SN_TYPE_FRWDENCAP && buf[0] + buf[buf[0]] != length) || 246 | (buf[1] != MQTT_SN_TYPE_FRWDENCAP && buf[0] != length)) { 247 | mqtt_sn_log_warn("Read %d bytes but packet length is %d bytes.", (int)length, 248 | buf[1] != MQTT_SN_TYPE_FRWDENCAP ? (int)buf[0] : (int)(buf[0] + buf[buf[0]])); 249 | return FALSE; 250 | } 251 | 252 | return TRUE; 253 | } 254 | 255 | void* mqtt_sn_receive_packet(int sock) 256 | { 257 | uint8_t *wireless_node_id = NULL; 258 | uint8_t wireless_node_id_len = 0; 259 | 260 | return mqtt_sn_receive_frwdencap_packet(sock, &wireless_node_id, &wireless_node_id_len); 261 | } 262 | 263 | void* mqtt_sn_receive_frwdencap_packet(int sock, uint8_t **wireless_node_id, uint8_t *wireless_node_id_len) 264 | { 265 | static uint8_t buffer[MQTT_SN_MAX_PACKET_LENGTH + MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH + 3 + 1]; 266 | struct sockaddr_storage addr; 267 | socklen_t slen = sizeof(addr); 268 | uint8_t *packet = buffer; 269 | ssize_t bytes_read; 270 | 271 | *wireless_node_id = NULL; 272 | *wireless_node_id_len = 0; 273 | 274 | mqtt_sn_log_debug("waiting for packet..."); 275 | 276 | // Read in the packet 277 | bytes_read = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, &slen); 278 | if (bytes_read < 0) { 279 | if (errno == EAGAIN) { 280 | mqtt_sn_log_debug("Timed out waiting for packet."); 281 | return NULL; 282 | } else { 283 | perror("recv failed"); 284 | exit(EXIT_FAILURE); 285 | } 286 | } 287 | 288 | // Convert the source address into a string 289 | if (debug) { 290 | char addrstr[INET6_ADDRSTRLEN] = "unknown"; 291 | uint16_t port = 0; 292 | 293 | if (addr.ss_family == AF_INET) { 294 | struct sockaddr_in *in = (struct sockaddr_in *)&addr; 295 | inet_ntop(AF_INET, &in->sin_addr, addrstr, sizeof(struct sockaddr_in)); 296 | port = ntohs(in->sin_port); 297 | } else if (addr.ss_family == AF_INET6) { 298 | struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)&addr; 299 | inet_ntop(AF_INET6, &in6->sin6_addr, addrstr, sizeof(struct sockaddr_in6)); 300 | port = ntohs(in6->sin6_port); 301 | } 302 | 303 | if (packet[1] == MQTT_SN_TYPE_FRWDENCAP) { 304 | mqtt_sn_log_debug("Received %2d bytes from %s:%d. Type=%s with %s inside on Socket: %d", 305 | (int)bytes_read, addrstr, port, 306 | mqtt_sn_type_string(buffer[1]), mqtt_sn_type_string(packet[packet[0] + 1]), sock); 307 | } else { 308 | mqtt_sn_log_debug("Received %2d bytes from %s:%d. Type=%s on Socket: %d", 309 | (int)bytes_read, addrstr, port, 310 | mqtt_sn_type_string(buffer[1]), sock); 311 | } 312 | } 313 | 314 | if (mqtt_sn_validate_packet(buffer, bytes_read) == FALSE) { 315 | return NULL; 316 | } 317 | 318 | // NULL-terminate the packet 319 | buffer[bytes_read] = '\0'; 320 | 321 | if (packet[1] == MQTT_SN_TYPE_FRWDENCAP) { 322 | *wireless_node_id = &packet[3]; 323 | *wireless_node_id_len = packet[0] - 3; 324 | // Shift packet by the actual length of FRWDENCAP header 325 | packet += packet[0]; 326 | } 327 | 328 | // Store the last time that we received a packet 329 | last_receive = time(NULL); 330 | 331 | return packet; 332 | } 333 | 334 | void mqtt_sn_send_connect(int sock, const char* client_id, uint16_t keepalive, uint8_t clean_session) 335 | { 336 | connect_packet_t packet; 337 | memset(&packet, 0, sizeof(packet)); 338 | 339 | // Check that it isn't too long 340 | if (client_id && strlen(client_id) > MQTT_SN_MAX_CLIENT_ID_LENGTH) { 341 | mqtt_sn_log_err("Client id is too long"); 342 | exit(EXIT_FAILURE); 343 | } 344 | 345 | // Create the CONNECT packet 346 | packet.type = MQTT_SN_TYPE_CONNECT; 347 | packet.flags = clean_session ? MQTT_SN_FLAG_CLEAN : 0; 348 | packet.protocol_id = MQTT_SN_PROTOCOL_ID; 349 | packet.duration = htons(keepalive); 350 | 351 | // Generate a Client ID if none given 352 | if (client_id == NULL || client_id[0] == '\0') { 353 | snprintf(packet.client_id, MQTT_SN_MAX_CLIENT_ID_LENGTH, "mqtt-sn-tools-%d", getpid()); 354 | } else { 355 | memcpy(packet.client_id, client_id, strlen(client_id)); 356 | } 357 | 358 | packet.length = 0x06 + strlen(packet.client_id); 359 | 360 | mqtt_sn_log_debug("Sending CONNECT packet..."); 361 | 362 | // Store the keep alive period 363 | if (keepalive) { 364 | keep_alive = keepalive; 365 | } 366 | 367 | mqtt_sn_send_packet(sock, &packet); 368 | } 369 | 370 | void mqtt_sn_send_register(int sock, const char* topic_name) 371 | { 372 | size_t topic_name_len = strlen(topic_name); 373 | register_packet_t packet; 374 | memset(&packet, 0, sizeof(packet)); 375 | 376 | if (topic_name_len > MQTT_SN_MAX_TOPIC_LENGTH) { 377 | mqtt_sn_log_err("Topic name is too long"); 378 | exit(EXIT_FAILURE); 379 | } 380 | 381 | packet.type = MQTT_SN_TYPE_REGISTER; 382 | packet.topic_id = 0; 383 | packet.message_id = htons(next_message_id++); 384 | strncpy(packet.topic_name, topic_name, sizeof(packet.topic_name)); 385 | packet.length = 0x06 + topic_name_len; 386 | 387 | mqtt_sn_log_debug("Sending REGISTER packet..."); 388 | 389 | mqtt_sn_send_packet(sock, &packet); 390 | } 391 | 392 | void mqtt_sn_send_regack(int sock, int topic_id, int mesage_id) 393 | { 394 | regack_packet_t packet; 395 | memset(&packet, 0, sizeof(packet)); 396 | 397 | packet.type = MQTT_SN_TYPE_REGACK; 398 | packet.topic_id = htons(topic_id); 399 | packet.message_id = htons(mesage_id); 400 | packet.return_code = 0x00; 401 | packet.length = 0x07; 402 | 403 | mqtt_sn_log_debug("Sending REGACK packet..."); 404 | 405 | mqtt_sn_send_packet(sock, &packet); 406 | } 407 | 408 | static uint8_t mqtt_sn_get_qos_flag(int8_t qos) 409 | { 410 | switch (qos) { 411 | case -1: 412 | return MQTT_SN_FLAG_QOS_N1; 413 | case 0: 414 | return MQTT_SN_FLAG_QOS_0; 415 | case 1: 416 | return MQTT_SN_FLAG_QOS_1; 417 | case 2: 418 | return MQTT_SN_FLAG_QOS_2; 419 | default: 420 | return 0; 421 | } 422 | } 423 | 424 | 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) 425 | { 426 | publish_packet_t packet; 427 | memset(&packet, 0, sizeof(packet)); 428 | 429 | if (data_len > sizeof(packet.data)) { 430 | mqtt_sn_log_err("Payload is too big"); 431 | exit(EXIT_FAILURE); 432 | } 433 | 434 | packet.type = MQTT_SN_TYPE_PUBLISH; 435 | packet.flags = 0x00; 436 | if (retain) 437 | packet.flags += MQTT_SN_FLAG_RETAIN; 438 | packet.flags += mqtt_sn_get_qos_flag(qos); 439 | packet.flags += (topic_type & 0x3); 440 | packet.topic_id = htons(topic_id); 441 | if (qos > 0) { 442 | packet.message_id = htons(next_message_id++); 443 | } else { 444 | packet.message_id = 0x0000; 445 | } 446 | memcpy(packet.data, data, sizeof(packet.data)); 447 | packet.length = 0x07 + data_len; 448 | 449 | mqtt_sn_log_debug("Sending PUBLISH packet..."); 450 | mqtt_sn_send_packet(sock, &packet); 451 | 452 | if (qos == 1) { 453 | // Now wait for a PUBACK 454 | puback_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_PUBACK, sock); 455 | if (packet) { 456 | mqtt_sn_log_debug("Received PUBACK"); 457 | } else { 458 | mqtt_sn_log_warn("Failed to receive PUBACK after PUBLISH"); 459 | } 460 | } 461 | } 462 | 463 | void mqtt_sn_send_puback(int sock, publish_packet_t* publish, uint8_t return_code) 464 | { 465 | puback_packet_t puback; 466 | memset(&puback, 0, sizeof(puback)); 467 | 468 | puback.type = MQTT_SN_TYPE_PUBACK; 469 | puback.topic_id = publish->topic_id; 470 | puback.message_id = publish->message_id; 471 | puback.return_code = return_code; 472 | puback.length = 0x07; 473 | 474 | mqtt_sn_log_debug("Sending PUBACK packet..."); 475 | 476 | mqtt_sn_send_packet(sock, &puback); 477 | } 478 | 479 | void mqtt_sn_send_subscribe_topic_name(int sock, const char* topic_name, uint8_t qos) 480 | { 481 | size_t topic_name_len = strlen(topic_name); 482 | subscribe_packet_t packet; 483 | memset(&packet, 0, sizeof(packet)); 484 | 485 | packet.type = MQTT_SN_TYPE_SUBSCRIBE; 486 | packet.flags = 0x00; 487 | packet.flags += mqtt_sn_get_qos_flag(qos); 488 | if (topic_name_len == 2) { 489 | packet.flags += MQTT_SN_TOPIC_TYPE_SHORT; 490 | } else { 491 | packet.flags += MQTT_SN_TOPIC_TYPE_NORMAL; 492 | } 493 | packet.message_id = htons(next_message_id++); 494 | strncpy(packet.topic_name, topic_name, sizeof(packet.topic_name)); 495 | packet.topic_name[sizeof(packet.topic_name)-1] = '\0'; 496 | packet.length = 0x05 + topic_name_len; 497 | 498 | mqtt_sn_log_debug("Sending SUBSCRIBE packet..."); 499 | 500 | mqtt_sn_send_packet(sock, &packet); 501 | } 502 | 503 | void mqtt_sn_send_subscribe_topic_id(int sock, uint16_t topic_id, uint8_t qos) 504 | { 505 | subscribe_packet_t packet; 506 | memset(&packet, 0, sizeof(packet)); 507 | 508 | packet.type = MQTT_SN_TYPE_SUBSCRIBE; 509 | packet.flags = 0x00; 510 | packet.flags += mqtt_sn_get_qos_flag(qos); 511 | packet.flags += MQTT_SN_TOPIC_TYPE_PREDEFINED; 512 | packet.message_id = htons(next_message_id++); 513 | packet.topic_id = htons(topic_id); 514 | packet.length = 0x05 + 2; 515 | 516 | mqtt_sn_log_debug("Sending SUBSCRIBE packet..."); 517 | 518 | mqtt_sn_send_packet(sock, &packet); 519 | } 520 | 521 | void mqtt_sn_send_pingreq(int sock) 522 | { 523 | char packet[2]; 524 | 525 | packet[0] = 2; 526 | packet[1] = MQTT_SN_TYPE_PINGREQ; 527 | 528 | mqtt_sn_log_debug("Sending PINGREQ packet..."); 529 | 530 | mqtt_sn_send_packet(sock, &packet); 531 | } 532 | 533 | void mqtt_sn_send_disconnect(int sock, uint16_t duration) 534 | { 535 | disconnect_packet_t packet; 536 | memset(&packet, 0, sizeof(packet)); 537 | 538 | packet.type = MQTT_SN_TYPE_DISCONNECT; 539 | if (duration == 0) { 540 | packet.length = 0x02; 541 | mqtt_sn_log_debug("Sending DISCONNECT packet..."); 542 | } else { 543 | packet.length = sizeof(packet); 544 | packet.duration = htons(duration); 545 | mqtt_sn_log_debug("Sending DISCONNECT packet with Duration %d...", duration); 546 | } 547 | 548 | mqtt_sn_send_packet(sock, &packet); 549 | } 550 | 551 | void mqtt_sn_receive_disconnect(int sock) 552 | { 553 | disconnect_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_DISCONNECT, sock); 554 | 555 | if (packet == NULL) { 556 | mqtt_sn_log_err("Failed to disconnect from MQTT-SN gateway."); 557 | exit(EXIT_FAILURE); 558 | } 559 | 560 | // Check Disconnect return duration 561 | if (packet->length == 4) { 562 | mqtt_sn_log_warn("DISCONNECT warning. Gateway returned duration in disconnect packet: 0x%2.2x", packet->duration); 563 | } 564 | } 565 | 566 | 567 | void mqtt_sn_receive_connack(int sock) 568 | { 569 | connack_packet_t *packet = mqtt_sn_receive_packet(sock); 570 | 571 | if (packet == NULL) { 572 | mqtt_sn_log_err("Failed to connect to MQTT-SN gateway."); 573 | exit(EXIT_FAILURE); 574 | } 575 | 576 | if (packet->type != MQTT_SN_TYPE_CONNACK) { 577 | mqtt_sn_log_err("Was expecting CONNACK packet but received: %s", mqtt_sn_type_string(packet->type)); 578 | exit(EXIT_FAILURE); 579 | } 580 | 581 | // Check Connack return code 582 | mqtt_sn_log_debug("CONNACK return code: 0x%2.2x", packet->return_code); 583 | 584 | if (packet->return_code) { 585 | mqtt_sn_log_err("CONNECT error: %s", mqtt_sn_return_code_string(packet->return_code)); 586 | exit(packet->return_code); 587 | } 588 | } 589 | 590 | static int mqtt_sn_process_register(int sock, const register_packet_t *packet) 591 | { 592 | int message_id = ntohs(packet->message_id); 593 | int topic_id = ntohs(packet->topic_id); 594 | const char* topic_name = packet->topic_name; 595 | 596 | // Add it to the topic map 597 | mqtt_sn_register_topic(topic_id, topic_name); 598 | 599 | // Respond to gateway with REGACK 600 | mqtt_sn_send_regack(sock, topic_id, message_id); 601 | 602 | return 0; 603 | } 604 | 605 | void mqtt_sn_register_topic(int topic_id, const char* topic_name) 606 | { 607 | topic_map_t **ptr = &topic_map; 608 | 609 | // Check topic ID is valid 610 | if (topic_id == 0x0000 || topic_id == 0xFFFF) { 611 | mqtt_sn_log_err("Attempted to register invalid topic id: 0x%4.4x", topic_id); 612 | return; 613 | } 614 | 615 | // Check topic name is valid 616 | if (topic_name == NULL || strlen(topic_name) <= 0) { 617 | mqtt_sn_log_err("Attempted to register invalid topic name."); 618 | return; 619 | } 620 | 621 | mqtt_sn_log_debug("Registering topic 0x%4.4x: %s", topic_id, topic_name); 622 | 623 | // Look for the topic id 624 | while (*ptr) { 625 | if ((*ptr)->topic_id == topic_id) { 626 | break; 627 | } else { 628 | ptr = &((*ptr)->next); 629 | } 630 | } 631 | 632 | // Allocate memory for a new entry, if we reached the end of the list 633 | if (*ptr == NULL) { 634 | *ptr = (topic_map_t *)malloc(sizeof(topic_map_t)); 635 | if (!*ptr) { 636 | mqtt_sn_log_err("Failed to allocate memory for new topic map entry."); 637 | exit(EXIT_FAILURE); 638 | } 639 | (*ptr)->next = NULL; 640 | } 641 | 642 | // Copy in the name to the entry 643 | strncpy((*ptr)->topic_name, topic_name, MQTT_SN_MAX_TOPIC_LENGTH); 644 | (*ptr)->topic_id = topic_id; 645 | } 646 | 647 | const char* mqtt_sn_lookup_topic(int topic_id) 648 | { 649 | topic_map_t **ptr = &topic_map; 650 | 651 | while (*ptr) { 652 | if ((*ptr)->topic_id == topic_id) { 653 | return (*ptr)->topic_name; 654 | } 655 | ptr = &((*ptr)->next); 656 | } 657 | 658 | mqtt_sn_log_warn("Failed to lookup topic id: 0x%4.4x", topic_id); 659 | return NULL; 660 | } 661 | 662 | uint16_t mqtt_sn_receive_regack(int sock) 663 | { 664 | regack_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_REGACK, sock); 665 | uint16_t received_message_id, received_topic_id; 666 | 667 | if (packet == NULL) { 668 | mqtt_sn_log_err("Failed to connect to register topic."); 669 | exit(EXIT_FAILURE); 670 | } 671 | 672 | // Check Regack return code 673 | mqtt_sn_log_debug("REGACK return code: 0x%2.2x", packet->return_code); 674 | 675 | if (packet->return_code) { 676 | mqtt_sn_log_err("REGISTER failed: %s", mqtt_sn_return_code_string(packet->return_code)); 677 | exit(packet->return_code); 678 | } 679 | 680 | // Check that the Message ID matches 681 | received_message_id = ntohs(packet->message_id); 682 | if (received_message_id != next_message_id-1) { 683 | mqtt_sn_log_warn("Message id in Regack does not equal message id sent"); 684 | } 685 | 686 | // Return the topic ID returned by the gateway 687 | received_topic_id = ntohs(packet->topic_id); 688 | mqtt_sn_log_debug("REGACK topic id: 0x%4.4x", received_topic_id); 689 | 690 | return received_topic_id; 691 | } 692 | 693 | void mqtt_sn_dump_packet(char* packet) 694 | { 695 | printf("%s: len=%d", mqtt_sn_type_string(packet[1]), packet[0]); 696 | 697 | switch(packet[1]) { 698 | case MQTT_SN_TYPE_CONNECT: { 699 | connect_packet_t* cpkt = (connect_packet_t*)packet; 700 | printf(" protocol_id=%d", cpkt->protocol_id); 701 | printf(" duration=%d", ntohs(cpkt->duration)); 702 | printf(" client_id=%s", cpkt->client_id); 703 | break; 704 | } 705 | case MQTT_SN_TYPE_CONNACK: { 706 | connack_packet_t* capkt = (connack_packet_t*)packet; 707 | printf(" return_code=%d (%s)", capkt->return_code, mqtt_sn_return_code_string(capkt->return_code)); 708 | break; 709 | } 710 | case MQTT_SN_TYPE_REGISTER: { 711 | register_packet_t* rpkt = (register_packet_t*)packet; 712 | printf(" topic_id=0x%4.4x", ntohs(rpkt->topic_id)); 713 | printf(" message_id=0x%4.4x", ntohs(rpkt->message_id)); 714 | printf(" topic_name=%s", rpkt->topic_name); 715 | break; 716 | } 717 | case MQTT_SN_TYPE_REGACK: { 718 | regack_packet_t* rapkt = (regack_packet_t*)packet; 719 | printf(" topic_id=0x%4.4x", ntohs(rapkt->topic_id)); 720 | printf(" message_id=0x%4.4x", ntohs(rapkt->message_id)); 721 | printf(" return_code=%d (%s)", rapkt->return_code, mqtt_sn_return_code_string(rapkt->return_code)); 722 | break; 723 | } 724 | case MQTT_SN_TYPE_PUBLISH: { 725 | publish_packet_t* ppkt = (publish_packet_t*)packet; 726 | printf(" topic_id=0x%4.4x", ntohs(ppkt->topic_id)); 727 | printf(" message_id=0x%4.4x", ntohs(ppkt->message_id)); 728 | printf(" data=%s", ppkt->data); 729 | break; 730 | } 731 | case MQTT_SN_TYPE_SUBSCRIBE: { 732 | subscribe_packet_t* spkt = (subscribe_packet_t*)packet; 733 | printf(" message_id=0x%4.4x", ntohs(spkt->message_id)); 734 | break; 735 | } 736 | case MQTT_SN_TYPE_SUBACK: { 737 | suback_packet_t* sapkt = (suback_packet_t*)packet; 738 | printf(" topic_id=0x%4.4x", ntohs(sapkt->topic_id)); 739 | printf(" message_id=0x%4.4x", ntohs(sapkt->message_id)); 740 | printf(" return_code=%d (%s)", sapkt->return_code, mqtt_sn_return_code_string(sapkt->return_code)); 741 | break; 742 | } 743 | case MQTT_SN_TYPE_DISCONNECT: { 744 | disconnect_packet_t* dpkt = (disconnect_packet_t*)packet; 745 | printf(" duration=%d", ntohs(dpkt->duration)); 746 | break; 747 | } 748 | } 749 | 750 | printf("\n"); 751 | } 752 | 753 | void mqtt_sn_print_publish_packet(publish_packet_t* packet) 754 | { 755 | if (verbose) { 756 | int topic_type = packet->flags & 0x3; 757 | int topic_id = ntohs(packet->topic_id); 758 | if (verbose == 2) { 759 | time_t rcv_time; 760 | char tm_buffer [40]; 761 | time(&rcv_time) ; 762 | strftime(tm_buffer, 40, "%F %T ", localtime(&rcv_time)); 763 | fputs(tm_buffer, stdout); 764 | } 765 | switch (topic_type) { 766 | case MQTT_SN_TOPIC_TYPE_NORMAL: { 767 | const char *topic_name = mqtt_sn_lookup_topic(topic_id); 768 | if (topic_name) { 769 | printf("%s: %s\n", topic_name, packet->data); 770 | } 771 | break; 772 | }; 773 | case MQTT_SN_TOPIC_TYPE_PREDEFINED: { 774 | printf("%4.4x: %s\n", topic_id, packet->data); 775 | break; 776 | }; 777 | case MQTT_SN_TOPIC_TYPE_SHORT: { 778 | const char *str = (const char*)&packet->topic_id; 779 | printf("%c%c: %s\n", str[0], str[1], packet->data); 780 | break; 781 | }; 782 | } 783 | } else { 784 | printf("%s\n", packet->data); 785 | } 786 | } 787 | 788 | uint16_t mqtt_sn_receive_suback(int sock) 789 | { 790 | suback_packet_t *packet = mqtt_sn_wait_for(MQTT_SN_TYPE_SUBACK, sock); 791 | uint16_t received_message_id, received_topic_id; 792 | 793 | if (packet == NULL) { 794 | mqtt_sn_log_err("Failed to subscribe to topic."); 795 | exit(EXIT_FAILURE); 796 | } 797 | 798 | // Check Suback return code 799 | mqtt_sn_log_debug("SUBACK return code: 0x%2.2x", packet->return_code); 800 | 801 | if (packet->return_code) { 802 | mqtt_sn_log_err("SUBSCRIBE error: %s", mqtt_sn_return_code_string(packet->return_code)); 803 | exit(packet->return_code); 804 | } 805 | 806 | // Check that the Message ID matches 807 | received_message_id = ntohs(packet->message_id); 808 | if (received_message_id != next_message_id-1) { 809 | mqtt_sn_log_warn("Message id in SUBACK does not equal message id sent"); 810 | mqtt_sn_log_debug(" Expecting: %d", next_message_id-1); 811 | mqtt_sn_log_debug(" Actual: %d", received_message_id); 812 | } 813 | 814 | // Return the topic ID returned by the gateway 815 | received_topic_id = ntohs(packet->topic_id); 816 | mqtt_sn_log_debug("SUBACK topic id: 0x%4.4x", received_topic_id); 817 | 818 | return received_topic_id; 819 | } 820 | 821 | int mqtt_sn_select(int sock) 822 | { 823 | struct timeval tv; 824 | fd_set rfd; 825 | int ret; 826 | 827 | FD_ZERO(&rfd); 828 | FD_SET(sock, &rfd); 829 | 830 | tv.tv_sec = timeout; 831 | tv.tv_usec = 0; 832 | 833 | ret = select(sock + 1, &rfd, NULL, NULL, &tv); 834 | if (ret < 0 && errno != EINTR) { 835 | // Something is wrong. 836 | perror("select"); 837 | exit(EXIT_FAILURE); 838 | } 839 | 840 | return ret; 841 | } 842 | 843 | void* mqtt_sn_wait_for(uint8_t type, int sock) 844 | { 845 | time_t started_waiting = time(NULL); 846 | 847 | while(TRUE) { 848 | time_t now = time(NULL); 849 | int ret; 850 | 851 | // Time to send a ping? 852 | if (keep_alive > 0 && (now - last_transmit) >= keep_alive) { 853 | mqtt_sn_send_pingreq(sock); 854 | } 855 | 856 | ret = mqtt_sn_select(sock); 857 | if (ret < 0) { 858 | break; 859 | } else if (ret > 0) { 860 | char* packet = mqtt_sn_receive_packet(sock); 861 | if (packet) { 862 | switch(packet[1]) { 863 | case MQTT_SN_TYPE_PUBLISH: 864 | mqtt_sn_print_publish_packet((publish_packet_t *)packet); 865 | break; 866 | 867 | case MQTT_SN_TYPE_REGISTER: 868 | mqtt_sn_process_register(sock, (register_packet_t*)packet); 869 | break; 870 | 871 | case MQTT_SN_TYPE_PINGRESP: 872 | // do nothing 873 | break; 874 | 875 | case MQTT_SN_TYPE_DISCONNECT: 876 | if (type != MQTT_SN_TYPE_DISCONNECT) { 877 | mqtt_sn_log_warn("Received DISCONNECT from gateway."); 878 | exit(EXIT_FAILURE); 879 | } 880 | break; 881 | 882 | default: 883 | if (packet[1] != type) { 884 | mqtt_sn_log_warn( 885 | "Was expecting %s packet but received: %s", 886 | mqtt_sn_type_string(type), 887 | mqtt_sn_type_string(packet[1]) 888 | ); 889 | } 890 | break; 891 | } 892 | 893 | // Did we find what we were looking for? 894 | if (packet[1] == type) { 895 | return packet; 896 | } 897 | } 898 | } 899 | 900 | // Check for receive timeout 901 | if (keep_alive > 0 && (now - last_receive) >= (keep_alive * 1.5)) { 902 | mqtt_sn_log_err("Keep alive error: timed out while waiting for a %s from gateway.", mqtt_sn_type_string(type)); 903 | exit(EXIT_FAILURE); 904 | } 905 | 906 | // Check if we have timed out waiting for the packet we are looking for 907 | if ((now - started_waiting) >= timeout) { 908 | mqtt_sn_log_debug("Timed out while waiting for a %s from gateway.", mqtt_sn_type_string(type)); 909 | break; 910 | } 911 | } 912 | 913 | return NULL; 914 | } 915 | 916 | const char* mqtt_sn_type_string(uint8_t type) 917 | { 918 | switch(type) { 919 | case MQTT_SN_TYPE_ADVERTISE: 920 | return "ADVERTISE"; 921 | case MQTT_SN_TYPE_SEARCHGW: 922 | return "SEARCHGW"; 923 | case MQTT_SN_TYPE_GWINFO: 924 | return "GWINFO"; 925 | case MQTT_SN_TYPE_CONNECT: 926 | return "CONNECT"; 927 | case MQTT_SN_TYPE_CONNACK: 928 | return "CONNACK"; 929 | case MQTT_SN_TYPE_WILLTOPICREQ: 930 | return "WILLTOPICREQ"; 931 | case MQTT_SN_TYPE_WILLTOPIC: 932 | return "WILLTOPIC"; 933 | case MQTT_SN_TYPE_WILLMSGREQ: 934 | return "WILLMSGREQ"; 935 | case MQTT_SN_TYPE_WILLMSG: 936 | return "WILLMSG"; 937 | case MQTT_SN_TYPE_REGISTER: 938 | return "REGISTER"; 939 | case MQTT_SN_TYPE_REGACK: 940 | return "REGACK"; 941 | case MQTT_SN_TYPE_PUBLISH: 942 | return "PUBLISH"; 943 | case MQTT_SN_TYPE_PUBACK: 944 | return "PUBACK"; 945 | case MQTT_SN_TYPE_PUBCOMP: 946 | return "PUBCOMP"; 947 | case MQTT_SN_TYPE_PUBREC: 948 | return "PUBREC"; 949 | case MQTT_SN_TYPE_PUBREL: 950 | return "PUBREL"; 951 | case MQTT_SN_TYPE_SUBSCRIBE: 952 | return "SUBSCRIBE"; 953 | case MQTT_SN_TYPE_SUBACK: 954 | return "SUBACK"; 955 | case MQTT_SN_TYPE_UNSUBSCRIBE: 956 | return "UNSUBSCRIBE"; 957 | case MQTT_SN_TYPE_UNSUBACK: 958 | return "UNSUBACK"; 959 | case MQTT_SN_TYPE_PINGREQ: 960 | return "PINGREQ"; 961 | case MQTT_SN_TYPE_PINGRESP: 962 | return "PINGRESP"; 963 | case MQTT_SN_TYPE_DISCONNECT: 964 | return "DISCONNECT"; 965 | case MQTT_SN_TYPE_WILLTOPICUPD: 966 | return "WILLTOPICUPD"; 967 | case MQTT_SN_TYPE_WILLTOPICRESP: 968 | return "WILLTOPICRESP"; 969 | case MQTT_SN_TYPE_WILLMSGUPD: 970 | return "WILLMSGUPD"; 971 | case MQTT_SN_TYPE_WILLMSGRESP: 972 | return "WILLMSGRESP"; 973 | case MQTT_SN_TYPE_FRWDENCAP: 974 | return "FRWDENCAP"; 975 | default: 976 | return "UNKNOWN"; 977 | } 978 | } 979 | 980 | const char* mqtt_sn_return_code_string(uint8_t return_code) 981 | { 982 | switch(return_code) { 983 | case MQTT_SN_ACCEPTED: 984 | return "Accepted"; 985 | case MQTT_SN_REJECTED_CONGESTION: 986 | return "Rejected: congestion"; 987 | case MQTT_SN_REJECTED_INVALID: 988 | return "Rejected: invalid topic ID"; 989 | case MQTT_SN_REJECTED_NOT_SUPPORTED: 990 | return "Rejected: not supported"; 991 | default: 992 | return "Rejected: unknown reason"; 993 | } 994 | } 995 | 996 | void mqtt_sn_cleanup() 997 | { 998 | topic_map_t *ptr = topic_map; 999 | topic_map_t *ptr2 = NULL; 1000 | 1001 | // Walk through the topic map, deleting each entry 1002 | while (ptr) { 1003 | ptr2 = ptr; 1004 | ptr = ptr->next; 1005 | free(ptr2); 1006 | } 1007 | topic_map = NULL; 1008 | } 1009 | 1010 | 1011 | uint8_t mqtt_sn_enable_frwdencap() 1012 | { 1013 | return forwarder_encapsulation = TRUE; 1014 | } 1015 | 1016 | 1017 | uint8_t mqtt_sn_disable_frwdencap() 1018 | { 1019 | return forwarder_encapsulation = FALSE; 1020 | } 1021 | 1022 | 1023 | void mqtt_sn_set_frwdencap_parameters(const uint8_t *wlnid, uint8_t wlnid_len) 1024 | { 1025 | wireless_node_id = wlnid; 1026 | wireless_node_id_len = wlnid_len; 1027 | } 1028 | 1029 | 1030 | 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) 1031 | { 1032 | frwdencap_packet_t* packet = NULL; 1033 | 1034 | // Check that it isn't too long 1035 | if (wireless_node_id_len > MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH) { 1036 | mqtt_sn_log_err("Wireless node id is longer than %d", MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH); 1037 | exit(EXIT_FAILURE); 1038 | } 1039 | 1040 | // Allocates a block of memory for an array of num elements, each of them size bytes long, 1041 | // and initializes all its bits to zero. 1042 | packet = malloc(sizeof(frwdencap_packet_t)); 1043 | packet->type = MQTT_SN_TYPE_FRWDENCAP; 1044 | packet->ctrl = 0; 1045 | 1046 | // Generate a Wireless Node ID if none given 1047 | if (wireless_node_id == NULL || wireless_node_id_len == 0) { 1048 | // A null character is automatically appended after the content written. 1049 | snprintf((char*)packet->wireless_node_id, sizeof(packet->wireless_node_id)-1, "%X", getpid()); 1050 | wireless_node_id_len = strlen((char*)packet->wireless_node_id); 1051 | } else { 1052 | memcpy(packet->wireless_node_id, wireless_node_id, wireless_node_id_len); 1053 | } 1054 | 1055 | packet->length = wireless_node_id_len + 3; 1056 | 1057 | // Copy mqtt-sn packet into forwarder encapsulation packet 1058 | memcpy(&(packet->wireless_node_id[wireless_node_id_len]), data, ((uint8_t*)data)[0]); 1059 | 1060 | // Set new packet length to send 1061 | *len = packet->length + ((uint8_t*)data)[0]; 1062 | 1063 | if (debug > 2) { 1064 | char wlnd[65]; 1065 | char* buf_ptr = wlnd; 1066 | int i; 1067 | for (i = 0; i < wireless_node_id_len; i++) { 1068 | buf_ptr += sprintf(buf_ptr, "%02X", packet->wireless_node_id[i]); 1069 | } 1070 | *(++buf_ptr) = '\0'; 1071 | 1072 | mqtt_sn_log_debug("Node id: 0x%s, N. id len: %d, Wrapped packet len: %d, Total len: %lu", 1073 | wlnd, wireless_node_id_len, ((uint8_t*)data)[0], (long unsigned int)*len); 1074 | } 1075 | 1076 | return packet; 1077 | } 1078 | 1079 | 1080 | static void mqtt_sn_log_msg(const char* level, const char* format, va_list arglist) 1081 | { 1082 | time_t mqtt_sn_log_time; 1083 | char tm_buffer[40]; 1084 | 1085 | time(&mqtt_sn_log_time); 1086 | strftime(tm_buffer, sizeof(tm_buffer), "%F %T ", localtime(&mqtt_sn_log_time)); 1087 | 1088 | fputs(tm_buffer, stderr); 1089 | fputs(level, stderr); 1090 | vfprintf(stderr, format, arglist); 1091 | fputs("\n", stderr); 1092 | } 1093 | 1094 | void mqtt_sn_log_debug(const char * format, ...) 1095 | { 1096 | if (debug) { 1097 | va_list arglist; 1098 | va_start(arglist, format); 1099 | mqtt_sn_log_msg("DEBUG ", format, arglist); 1100 | va_end(arglist); 1101 | } 1102 | } 1103 | 1104 | void mqtt_sn_log_warn(const char * format, ...) 1105 | { 1106 | va_list arglist; 1107 | va_start(arglist, format); 1108 | mqtt_sn_log_msg("WARN ", format, arglist); 1109 | va_end(arglist); 1110 | } 1111 | 1112 | void mqtt_sn_log_err(const char * format, ...) 1113 | { 1114 | va_list arglist; 1115 | va_start(arglist, format); 1116 | mqtt_sn_log_msg("ERROR ", format, arglist); 1117 | va_end(arglist); 1118 | } 1119 | -------------------------------------------------------------------------------- /mqtt-sn.h: -------------------------------------------------------------------------------- 1 | /* 2 | Common functions used by the MQTT-SN Tools 3 | Copyright (C) Nicholas Humfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | #include 25 | 26 | #ifndef MQTT_SN_H 27 | #define MQTT_SN_H 28 | 29 | #ifndef FALSE 30 | #define FALSE (0) 31 | #endif 32 | 33 | #ifndef TRUE 34 | #define TRUE (1) 35 | #endif 36 | 37 | #define MQTT_SN_DEFAULT_PORT "1883" 38 | #define MQTT_SN_DEFAULT_TIMEOUT (10) 39 | #define MQTT_SN_DEFAULT_KEEP_ALIVE (10) 40 | 41 | #define MQTT_SN_MAX_PACKET_LENGTH (255) 42 | #define MQTT_SN_MAX_PAYLOAD_LENGTH (MQTT_SN_MAX_PACKET_LENGTH-7) 43 | #define MQTT_SN_MAX_TOPIC_LENGTH (MQTT_SN_MAX_PACKET_LENGTH-6) 44 | #define MQTT_SN_MAX_CLIENT_ID_LENGTH (23) 45 | #define MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH (252) 46 | 47 | #define MQTT_SN_TYPE_ADVERTISE (0x00) 48 | #define MQTT_SN_TYPE_SEARCHGW (0x01) 49 | #define MQTT_SN_TYPE_GWINFO (0x02) 50 | #define MQTT_SN_TYPE_CONNECT (0x04) 51 | #define MQTT_SN_TYPE_CONNACK (0x05) 52 | #define MQTT_SN_TYPE_WILLTOPICREQ (0x06) 53 | #define MQTT_SN_TYPE_WILLTOPIC (0x07) 54 | #define MQTT_SN_TYPE_WILLMSGREQ (0x08) 55 | #define MQTT_SN_TYPE_WILLMSG (0x09) 56 | #define MQTT_SN_TYPE_REGISTER (0x0A) 57 | #define MQTT_SN_TYPE_REGACK (0x0B) 58 | #define MQTT_SN_TYPE_PUBLISH (0x0C) 59 | #define MQTT_SN_TYPE_PUBACK (0x0D) 60 | #define MQTT_SN_TYPE_PUBCOMP (0x0E) 61 | #define MQTT_SN_TYPE_PUBREC (0x0F) 62 | #define MQTT_SN_TYPE_PUBREL (0x10) 63 | #define MQTT_SN_TYPE_SUBSCRIBE (0x12) 64 | #define MQTT_SN_TYPE_SUBACK (0x13) 65 | #define MQTT_SN_TYPE_UNSUBSCRIBE (0x14) 66 | #define MQTT_SN_TYPE_UNSUBACK (0x15) 67 | #define MQTT_SN_TYPE_PINGREQ (0x16) 68 | #define MQTT_SN_TYPE_PINGRESP (0x17) 69 | #define MQTT_SN_TYPE_DISCONNECT (0x18) 70 | #define MQTT_SN_TYPE_WILLTOPICUPD (0x1A) 71 | #define MQTT_SN_TYPE_WILLTOPICRESP (0x1B) 72 | #define MQTT_SN_TYPE_WILLMSGUPD (0x1C) 73 | #define MQTT_SN_TYPE_WILLMSGRESP (0x1D) 74 | #define MQTT_SN_TYPE_FRWDENCAP (0xFE) 75 | 76 | #define MQTT_SN_ACCEPTED (0x00) 77 | #define MQTT_SN_REJECTED_CONGESTION (0x01) 78 | #define MQTT_SN_REJECTED_INVALID (0x02) 79 | #define MQTT_SN_REJECTED_NOT_SUPPORTED (0x03) 80 | 81 | #define MQTT_SN_TOPIC_TYPE_NORMAL (0x00) 82 | #define MQTT_SN_TOPIC_TYPE_PREDEFINED (0x01) 83 | #define MQTT_SN_TOPIC_TYPE_SHORT (0x02) 84 | 85 | 86 | #define MQTT_SN_FLAG_DUP (0x1 << 7) 87 | #define MQTT_SN_FLAG_QOS_0 (0x0 << 5) 88 | #define MQTT_SN_FLAG_QOS_1 (0x1 << 5) 89 | #define MQTT_SN_FLAG_QOS_2 (0x2 << 5) 90 | #define MQTT_SN_FLAG_QOS_N1 (0x3 << 5) 91 | #define MQTT_SN_FLAG_QOS_MASK (0x3 << 5) 92 | #define MQTT_SN_FLAG_RETAIN (0x1 << 4) 93 | #define MQTT_SN_FLAG_WILL (0x1 << 3) 94 | #define MQTT_SN_FLAG_CLEAN (0x1 << 2) 95 | 96 | #define MQTT_SN_PROTOCOL_ID (0x01) 97 | 98 | typedef struct { 99 | uint8_t length; 100 | uint8_t type; 101 | uint8_t flags; 102 | uint8_t protocol_id; 103 | uint16_t duration; 104 | char client_id[MQTT_SN_MAX_CLIENT_ID_LENGTH]; 105 | } connect_packet_t; 106 | 107 | typedef struct { 108 | uint8_t length; 109 | uint8_t type; 110 | uint8_t return_code; 111 | } connack_packet_t; 112 | 113 | typedef struct { 114 | uint8_t length; 115 | uint8_t type; 116 | uint16_t topic_id; 117 | uint16_t message_id; 118 | char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; 119 | } register_packet_t; 120 | 121 | typedef struct { 122 | uint8_t length; 123 | uint8_t type; 124 | uint16_t topic_id; 125 | uint16_t message_id; 126 | uint8_t return_code; 127 | } regack_packet_t; 128 | 129 | typedef struct __attribute__((packed)) { 130 | uint8_t length; 131 | uint8_t type; 132 | uint8_t flags; 133 | uint16_t topic_id; 134 | uint16_t message_id; 135 | char data[MQTT_SN_MAX_PAYLOAD_LENGTH]; 136 | } 137 | publish_packet_t; 138 | 139 | typedef struct __attribute__((packed)) { 140 | uint8_t length; 141 | uint8_t type; 142 | uint16_t topic_id; 143 | uint16_t message_id; 144 | uint8_t return_code; 145 | } 146 | puback_packet_t; 147 | 148 | typedef struct __attribute__((packed)) { 149 | uint8_t length; 150 | uint8_t type; 151 | uint8_t flags; 152 | uint16_t message_id; 153 | union { 154 | char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; 155 | uint16_t topic_id; 156 | }; 157 | } 158 | subscribe_packet_t; 159 | 160 | typedef struct __attribute__((packed)) { 161 | uint8_t length; 162 | uint8_t type; 163 | uint8_t flags; 164 | uint16_t topic_id; 165 | uint16_t message_id; 166 | uint8_t return_code; 167 | } 168 | suback_packet_t; 169 | 170 | typedef struct { 171 | uint8_t length; 172 | uint8_t type; 173 | uint16_t duration; 174 | } disconnect_packet_t; 175 | 176 | typedef struct __attribute__((packed)) { 177 | uint8_t length; 178 | uint8_t type; 179 | uint8_t ctrl; 180 | uint8_t wireless_node_id[MQTT_SN_MAX_WIRELESS_NODE_ID_LENGTH]; 181 | char data[MQTT_SN_MAX_PACKET_LENGTH]; 182 | } 183 | frwdencap_packet_t; 184 | 185 | typedef struct topic_map { 186 | uint16_t topic_id; 187 | char topic_name[MQTT_SN_MAX_TOPIC_LENGTH]; 188 | struct topic_map *next; 189 | } topic_map_t; 190 | 191 | 192 | // Library functions 193 | int mqtt_sn_create_socket(const char* host, const char* port, uint16_t source_port); 194 | void mqtt_sn_send_connect(int sock, const char* client_id, uint16_t keepalive, uint8_t clean_session); 195 | void mqtt_sn_send_register(int sock, const char* topic_name); 196 | 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); 197 | void mqtt_sn_send_puback(int sock, publish_packet_t* publish, uint8_t return_code); 198 | void mqtt_sn_send_subscribe_topic_name(int sock, const char* topic_name, uint8_t qos); 199 | void mqtt_sn_send_subscribe_topic_id(int sock, uint16_t topic_id, uint8_t qos); 200 | void mqtt_sn_send_pingreq(int sock); 201 | void mqtt_sn_send_disconnect(int sock, uint16_t duration); 202 | void mqtt_sn_receive_disconnect(int sock); 203 | void mqtt_sn_receive_connack(int sock); 204 | uint16_t mqtt_sn_receive_regack(int sock); 205 | uint16_t mqtt_sn_receive_suback(int sock); 206 | void mqtt_sn_dump_packet(char* packet); 207 | void mqtt_sn_print_publish_packet(publish_packet_t* packet); 208 | int mqtt_sn_select(int sock); 209 | void* mqtt_sn_wait_for(uint8_t type, int sock); 210 | void mqtt_sn_register_topic(int topic_id, const char* topic_name); 211 | const char* mqtt_sn_lookup_topic(int topic_id); 212 | void mqtt_sn_cleanup(); 213 | 214 | void mqtt_sn_set_debug(uint8_t value); 215 | void mqtt_sn_set_verbose(uint8_t value); 216 | void mqtt_sn_set_timeout(uint8_t value); 217 | const char* mqtt_sn_type_string(uint8_t type); 218 | const char* mqtt_sn_return_code_string(uint8_t return_code); 219 | 220 | uint8_t mqtt_sn_validate_packet(const void *packet, size_t length); 221 | void mqtt_sn_send_packet(int sock, const void* data); 222 | void mqtt_sn_send_frwdencap_packet(int sock, const void* data, const uint8_t *wireless_node_id, uint8_t wireless_node_id_len); 223 | void* mqtt_sn_receive_packet(int sock); 224 | void* mqtt_sn_receive_frwdencap_packet(int sock, uint8_t **wireless_node_id, uint8_t *wireless_node_id_len); 225 | 226 | // Functions to turn on and off forwarder encapsulation according to MQTT-SN Protocol Specification v1.2, 227 | // chapter 5.5 Forwarder Encapsulation. 228 | uint8_t mqtt_sn_enable_frwdencap(); 229 | uint8_t mqtt_sn_disable_frwdencap(); 230 | 231 | // Set wireless node ID and wireless node ID length 232 | void mqtt_sn_set_frwdencap_parameters(const uint8_t *wlnid, uint8_t wlnid_len); 233 | 234 | // Wrap mqtt-sn packet into a forwarder encapsulation packet 235 | 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); 236 | 237 | void mqtt_sn_log_debug(const char * format, ...); 238 | void mqtt_sn_log_warn(const char * format, ...); 239 | void mqtt_sn_log_err(const char * format, ...); 240 | 241 | #endif 242 | -------------------------------------------------------------------------------- /test/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.5 -------------------------------------------------------------------------------- /test/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby File.read('.ruby-version').chomp 3 | 4 | gem 'bundler', '>= 1.5.0' 5 | gem 'rake', '>= 0.10.0' 6 | gem 'mqtt', '>= 0.4.0' 7 | gem 'minitest', '>= 5.0.0' 8 | -------------------------------------------------------------------------------- /test/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | minitest (5.14.3) 5 | mqtt (0.5.0) 6 | rake (13.0.3) 7 | 8 | PLATFORMS 9 | ruby 10 | x86_64-darwin-18 11 | 12 | DEPENDENCIES 13 | bundler (>= 1.5.0) 14 | minitest (>= 5.0.0) 15 | mqtt (>= 0.4.0) 16 | rake (>= 0.10.0) 17 | 18 | RUBY VERSION 19 | ruby 2.5.5p157 20 | 21 | BUNDLED WITH 22 | 2.2.0 23 | -------------------------------------------------------------------------------- /test/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new do |t| 7 | t.pattern = "*-test.rb" 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /test/fake_server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This is a 'fake' MQTT-SN server to help with testing client implementations 4 | # 5 | # It behaves in the following ways: 6 | # * Responds to CONNECT with a successful CONACK 7 | # * Responds to PUBLISH by keeping a copy of the packet 8 | # * Responds to SUBSCRIBE with SUBACK and a PUBLISH to the topic 9 | # * Responds to PINGREQ with PINGRESP and keeps a count 10 | # * Responds to DISCONNECT with a DISCONNECT packet 11 | # 12 | # It has the following restrictions 13 | # * Doesn't deal with timeouts 14 | # * Only handles a single client 15 | # 16 | 17 | require 'logger' 18 | require 'socket' 19 | require 'mqtt' 20 | 21 | 22 | class MQTT::SN::FakeServer 23 | attr_reader :address, :port 24 | attr_reader :thread 25 | attr_reader :packets_received 26 | 27 | # Create a new fake MQTT server 28 | # 29 | # If no port is given, bind to a random port number 30 | # If no bind address is given, bind to localhost 31 | def initialize(port=0, bind_address='127.0.0.1') 32 | @port = port 33 | @address = bind_address 34 | @packets_received = [] 35 | end 36 | 37 | # Get the logger used by the server 38 | def logger 39 | @logger ||= Logger.new(STDOUT) 40 | end 41 | 42 | def logger=(logger) 43 | @logger = logger 44 | end 45 | 46 | # Start the thread and open the socket that will process client connections 47 | def start 48 | @thread ||= Thread.new do 49 | Thread.current.abort_on_exception = true 50 | Socket.udp_server_sockets(@address, @port) do |sockets| 51 | @address = sockets.first.local_address.ip_address 52 | @port = sockets.first.local_address.ip_port 53 | logger.info "Started a fake MQTT-SN server on #{@address}:#{@port}" 54 | Socket.udp_server_loop_on(sockets) do |data, client| 55 | response = process_packet(data) 56 | unless response.nil? 57 | response = [response] unless response.kind_of?(Enumerable) 58 | response.each do |packet| 59 | logger.debug "Replying with: #{packet.inspect}" 60 | client.reply(packet.to_s) 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | # Stop the thread and close the socket 69 | def stop 70 | logger.info "Stopping fake MQTT-SN server" 71 | @thread.kill if @thread and @thread.alive? 72 | @thread = nil 73 | end 74 | 75 | # Start the server thread and wait for it to finish (possibly never) 76 | def run 77 | start 78 | begin 79 | @thread.join 80 | rescue Interrupt 81 | stop 82 | end 83 | end 84 | 85 | def wait_for_port_number 86 | while @port.to_i == 0 87 | Thread.pass 88 | end 89 | @port 90 | end 91 | 92 | def wait_for_packet(klass=nil, timeout=3) 93 | begin 94 | Timeout.timeout(timeout) do 95 | if block_given? 96 | @packets_received = [] 97 | yield 98 | end 99 | loop do 100 | if klass.nil? 101 | unless @packets_received.empty? 102 | return @packets_received.last_packet 103 | end 104 | else 105 | @packets_received.each do |packet| 106 | return packet if packet.class == klass 107 | end 108 | end 109 | sleep(0.01) 110 | end 111 | end 112 | rescue Timeout::Error 113 | logger.warn "FakeServer timed out waiting for a #{klass}" 114 | return nil 115 | end 116 | end 117 | 118 | protected 119 | 120 | def process_packet(data) 121 | packet = MQTT::SN::Packet.parse(data) 122 | @packets_received << packet 123 | logger.debug "Received: #{packet.inspect}" 124 | 125 | method = 'handle_' + packet.class.name.split('::').last.downcase 126 | if respond_to?(method, true) 127 | send(method, packet) 128 | else 129 | logger.warn "Unhandled packet type: #{packet.class}" 130 | nil 131 | end 132 | 133 | rescue MQTT::SN::ProtocolException => e 134 | logger.warn "Protocol error: #{e}" 135 | nil 136 | end 137 | 138 | def handle_connect(packet) 139 | MQTT::SN::Packet::Connack.new(:return_code => 0x00) 140 | end 141 | 142 | def handle_publish(packet) 143 | if packet.qos > 0 144 | MQTT::SN::Packet::Puback.new( 145 | :id => packet.id, 146 | :topic_id => packet.topic_id, 147 | :return_code => 0x00 148 | ) 149 | end 150 | end 151 | 152 | def handle_pingreq(packet) 153 | MQTT::SN::Packet::Pingresp.new 154 | end 155 | 156 | def handle_subscribe(packet, publish_data=nil) 157 | case packet.topic_id_type 158 | when :short 159 | topic_id = packet.topic_name 160 | publish_data ||= "Message for #{packet.topic_name}" 161 | when :predefined 162 | topic_id = packet.topic_id 163 | publish_data ||= "Message for ##{packet.topic_id}" 164 | when :normal 165 | topic_id = 1 166 | publish_data ||= "Message for #{packet.topic_name}" 167 | else 168 | logger.warn "Unknown Topic Id Type: #{packet.topic_id_type}" 169 | end 170 | [ 171 | MQTT::SN::Packet::Suback.new( 172 | :id => packet.id, 173 | :topic_id_type => packet.topic_id_type, 174 | :topic_id => topic_id, 175 | :return_code => 0 176 | ), 177 | MQTT::SN::Packet::Publish.new( 178 | :topic_id_type => packet.topic_id_type, 179 | :topic_id => topic_id, 180 | :qos => packet.qos, 181 | :data => publish_data 182 | ) 183 | ] 184 | end 185 | 186 | def handle_disconnect(packet) 187 | MQTT::SN::Packet::Disconnect.new 188 | end 189 | 190 | def handle_register(packet) 191 | MQTT::SN::Packet::Regack.new( 192 | :id => packet.id, 193 | :topic_id => 1, 194 | :return_code => 0x00 195 | ) 196 | end 197 | 198 | def handle_puback(packet) 199 | nil 200 | end 201 | 202 | def handle_regack(packet) 203 | nil 204 | end 205 | 206 | end 207 | 208 | if __FILE__ == $0 209 | server = MQTT::SN::FakeServer.new(MQTT::SN::DEFAULT_PORT) 210 | server.logger.level = Logger::DEBUG 211 | server.run 212 | end 213 | -------------------------------------------------------------------------------- /test/mqtt-sn-dump-test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'test_helper' 4 | 5 | class MqttSnDumpTest < Minitest::Test 6 | 7 | def test_usage 8 | @cmd_result = run_cmd('mqtt-sn-dump', '-?') 9 | assert_match(/^Usage: mqtt-sn-dump/, @cmd_result[0]) 10 | end 11 | 12 | def publish_packet(port, packet) 13 | # FIXME: better way to wait until socket is open? 14 | sleep 0.2 15 | socket = UDPSocket.new 16 | socket.connect('localhost', port) 17 | socket << packet.to_s 18 | socket.close 19 | end 20 | 21 | def publish_qos_n1_packet(port) 22 | publish_packet(port, 23 | MQTT::SN::Packet::Publish.new( 24 | :topic_id => 'TT', 25 | :topic_id_type => :short, 26 | :data => "Message for TT", 27 | :qos => -1 28 | ) 29 | ) 30 | end 31 | 32 | def test_receive_qos_n1 33 | @port = random_port 34 | @cmd_result = run_cmd( 35 | 'mqtt-sn-dump', 36 | ['-p', @port] 37 | ) do |cmd| 38 | publish_qos_n1_packet(@port) 39 | wait_for_output_then_kill(cmd) 40 | end 41 | 42 | assert_equal(["Message for TT"], @cmd_result) 43 | end 44 | 45 | def test_receive_qos_n1_debug 46 | @port = random_port 47 | @cmd_result = run_cmd( 48 | 'mqtt-sn-dump', 49 | ['-d', '-p', @port] 50 | ) do |cmd| 51 | publish_qos_n1_packet(@port) 52 | wait_for_output_then_kill(cmd) 53 | end 54 | 55 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG mqtt-sn-dump listening on port \d+/, @cmd_result) 56 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) 57 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Received 21 bytes from 127.0.0.1\:\d+. Type=PUBLISH/, @cmd_result) 58 | end 59 | 60 | def test_receive_qos_n1_verbose 61 | @port = random_port 62 | @cmd_result = run_cmd( 63 | 'mqtt-sn-dump', 64 | ['-v', '-p', @port] 65 | ) do |cmd| 66 | publish_qos_n1_packet(@port) 67 | wait_for_output_then_kill(cmd) 68 | end 69 | 70 | assert_equal(["TT: Message for TT"], @cmd_result) 71 | end 72 | 73 | def test_receive_qos_n1_dump_all 74 | @port = random_port 75 | @cmd_result = run_cmd( 76 | 'mqtt-sn-dump', 77 | ['-a', '-p', @port] 78 | ) do |cmd| 79 | publish_qos_n1_packet(@port) 80 | wait_for_output_then_kill(cmd) 81 | end 82 | 83 | assert_equal(["PUBLISH: len=21 topic_id=0x5454 message_id=0x0000 data=Message for TT"], @cmd_result) 84 | end 85 | 86 | def test_receive_qos_n1_term 87 | @port = random_port 88 | @cmd_result = run_cmd( 89 | 'mqtt-sn-dump', 90 | ['-p', @port] 91 | ) do |cmd| 92 | publish_qos_n1_packet(@port) 93 | wait_for_output_then_kill(cmd, 'TERM') 94 | end 95 | 96 | assert_equal(["Message for TT"], @cmd_result) 97 | end 98 | 99 | def test_receive_qos_n1_hup 100 | @port = random_port 101 | @cmd_result = run_cmd( 102 | 'mqtt-sn-dump', 103 | ['-p', @port] 104 | ) do |cmd| 105 | publish_qos_n1_packet(@port) 106 | wait_for_output_then_kill(cmd, 'HUP') 107 | end 108 | 109 | assert_equal(["Message for TT"], @cmd_result) 110 | end 111 | 112 | def test_receive_connect 113 | @port = random_port 114 | @cmd_result = run_cmd( 115 | 'mqtt-sn-dump', 116 | ['-a', '-p', @port] 117 | ) do |cmd| 118 | publish_packet(@port, 119 | MQTT::SN::Packet::Connect.new( 120 | :client_id => 'my_client_id', 121 | :keep_alive => 10 122 | ) 123 | ) 124 | wait_for_output_then_kill(cmd) 125 | end 126 | 127 | assert_equal(["CONNECT: len=18 protocol_id=1 duration=10 client_id=my_client_id"], @cmd_result) 128 | end 129 | 130 | def test_receive_connack 131 | @port = random_port 132 | @cmd_result = run_cmd( 133 | 'mqtt-sn-dump', 134 | ['-a', '-p', @port] 135 | ) do |cmd| 136 | publish_packet(@port, 137 | MQTT::SN::Packet::Connack.new( 138 | :return_code => 1 139 | ) 140 | ) 141 | wait_for_output_then_kill(cmd) 142 | end 143 | 144 | assert_equal(["CONNACK: len=3 return_code=1 (Rejected: congestion)"], @cmd_result) 145 | end 146 | 147 | def test_receive_connack_not_supported 148 | @port = random_port 149 | @cmd_result = run_cmd( 150 | 'mqtt-sn-dump', 151 | ['-a', '-p', @port] 152 | ) do |cmd| 153 | publish_packet(@port, 154 | MQTT::SN::Packet::Connack.new( 155 | :return_code => 3 156 | ) 157 | ) 158 | wait_for_output_then_kill(cmd) 159 | end 160 | 161 | assert_equal(["CONNACK: len=3 return_code=3 (Rejected: not supported)"], @cmd_result) 162 | end 163 | 164 | def test_receive_register 165 | @port = random_port 166 | @cmd_result = run_cmd( 167 | 'mqtt-sn-dump', 168 | ['-a', '-p', @port] 169 | ) do |cmd| 170 | publish_packet(@port, 171 | MQTT::SN::Packet::Register.new( 172 | :id => 10, 173 | :topic_id => 20, 174 | :topic_name => 'Topic Name' 175 | ) 176 | ) 177 | wait_for_output_then_kill(cmd) 178 | end 179 | 180 | assert_equal(["REGISTER: len=16 topic_id=0x0014 message_id=0x000a topic_name=Topic Name"], @cmd_result) 181 | end 182 | 183 | def test_receive_regack 184 | @port = random_port 185 | @cmd_result = run_cmd( 186 | 'mqtt-sn-dump', 187 | ['-a', '-p', @port] 188 | ) do |cmd| 189 | publish_packet(@port, 190 | MQTT::SN::Packet::Regack.new( 191 | :id => 30, 192 | :topic_id => 40, 193 | :return_code => 0 194 | ) 195 | ) 196 | wait_for_output_then_kill(cmd) 197 | end 198 | 199 | assert_equal(["REGACK: len=7 topic_id=0x0028 message_id=0x001e return_code=0 (Accepted)"], @cmd_result) 200 | end 201 | 202 | def test_receive_subscribe 203 | @port = random_port 204 | @cmd_result = run_cmd( 205 | 'mqtt-sn-dump', 206 | ['-a', '-p', @port] 207 | ) do |cmd| 208 | publish_packet(@port, 209 | MQTT::SN::Packet::Subscribe.new(:id => 50) 210 | ) 211 | wait_for_output_then_kill(cmd) 212 | end 213 | 214 | assert_equal(["SUBSCRIBE: len=5 message_id=0x0032"], @cmd_result) 215 | end 216 | 217 | def test_receive_suback 218 | @port = random_port 219 | @cmd_result = run_cmd( 220 | 'mqtt-sn-dump', 221 | ['-a', '-p', @port] 222 | ) do |cmd| 223 | publish_packet(@port, 224 | MQTT::SN::Packet::Suback.new( 225 | :id => 60, 226 | :topic_id => 70, 227 | :return_code => 0 228 | ) 229 | ) 230 | wait_for_output_then_kill(cmd) 231 | end 232 | 233 | assert_equal(["SUBACK: len=8 topic_id=0x0046 message_id=0x003c return_code=0 (Accepted)"], @cmd_result) 234 | end 235 | 236 | def test_receive_suback_unknown_error 237 | @port = random_port 238 | @cmd_result = run_cmd( 239 | 'mqtt-sn-dump', 240 | ['-a', '-p', @port] 241 | ) do |cmd| 242 | publish_packet(@port, 243 | MQTT::SN::Packet::Suback.new( 244 | :id => 60, 245 | :topic_id => 70, 246 | :return_code => 5 247 | ) 248 | ) 249 | wait_for_output_then_kill(cmd) 250 | end 251 | 252 | assert_equal(["SUBACK: len=8 topic_id=0x0046 message_id=0x003c return_code=5 (Rejected: unknown reason)"], @cmd_result) 253 | end 254 | 255 | def test_receive_pingreq 256 | @port = random_port 257 | @cmd_result = run_cmd( 258 | 'mqtt-sn-dump', 259 | ['-a', '-p', @port] 260 | ) do |cmd| 261 | publish_packet(@port, MQTT::SN::Packet::Pingreq.new) 262 | wait_for_output_then_kill(cmd) 263 | end 264 | 265 | assert_equal(["PINGREQ: len=2"], @cmd_result) 266 | end 267 | 268 | def test_receive_pingresp 269 | @port = random_port 270 | @cmd_result = run_cmd( 271 | 'mqtt-sn-dump', 272 | ['-a', '-p', @port] 273 | ) do |cmd| 274 | publish_packet(@port, MQTT::SN::Packet::Pingresp.new) 275 | wait_for_output_then_kill(cmd) 276 | end 277 | 278 | assert_equal(["PINGRESP: len=2"], @cmd_result) 279 | end 280 | 281 | def test_receive_disconnect 282 | @port = random_port 283 | @cmd_result = run_cmd( 284 | 'mqtt-sn-dump', 285 | ['-a', '-p', @port] 286 | ) do |cmd| 287 | publish_packet(@port, MQTT::SN::Packet::Disconnect.new) 288 | wait_for_output_then_kill(cmd) 289 | end 290 | 291 | assert_equal(["DISCONNECT: len=2 duration=0"], @cmd_result) 292 | end 293 | 294 | def test_receive_unknown 295 | @port = random_port 296 | @cmd_result = run_cmd( 297 | 'mqtt-sn-dump', 298 | ['-a', '-p', @port] 299 | ) do |cmd| 300 | publish_packet(@port, "\x03\xFF\x00") 301 | wait_for_output_then_kill(cmd) 302 | end 303 | 304 | assert_equal(["UNKNOWN: len=3"], @cmd_result) 305 | end 306 | 307 | def test_receive_invalid_length 308 | @port = random_port 309 | @cmd_result = run_cmd( 310 | 'mqtt-sn-dump', 311 | ['-a', '-p', @port] 312 | ) do |cmd| 313 | publish_packet(@port, "\x00\x00\x00\x00") 314 | wait_for_output_then_kill(cmd) 315 | end 316 | 317 | assert_match(/WARN Packet length header is not valid/, @cmd_result[0]) 318 | end 319 | 320 | end 321 | -------------------------------------------------------------------------------- /test/mqtt-sn-pub-test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'test_helper' 4 | 5 | class MqttSnPubTest < Minitest::Test 6 | 7 | def test_usage 8 | @cmd_result = run_cmd('mqtt-sn-pub', '-?') 9 | assert_match(/^Usage: mqtt-sn-pub/, @cmd_result[0]) 10 | end 11 | 12 | def test_no_arguments 13 | @cmd_result = run_cmd('mqtt-sn-pub') 14 | assert_match(/^Usage: mqtt-sn-pub/, @cmd_result[0]) 15 | end 16 | 17 | def test_default_client_id 18 | fake_server do |fs| 19 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 20 | @cmd_result = run_cmd( 21 | 'mqtt-sn-pub', 22 | '-T' => 10, 23 | '-m' => 'message', 24 | '-p' => fs.port, 25 | '-h' => fs.address 26 | ) 27 | end 28 | end 29 | 30 | assert_empty(@cmd_result) 31 | assert_match(/^mqtt-sn-tools-(\d+)$/, @packet.client_id) 32 | assert_equal(10, @packet.keep_alive) 33 | assert_equal(true, @packet.clean_session) 34 | end 35 | 36 | def test_custom_client_id 37 | fake_server do |fs| 38 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 39 | @cmd_result = run_cmd( 40 | 'mqtt-sn-pub', 41 | '-i' => 'test_custom_client_id', 42 | '-T' => 10, 43 | '-m' => 'message', 44 | '-p' => fs.port, 45 | '-h' => fs.address 46 | ) 47 | end 48 | end 49 | 50 | assert_empty(@cmd_result) 51 | assert_equal('test_custom_client_id', @packet.client_id) 52 | assert_equal(10, @packet.keep_alive) 53 | assert_equal(true, @packet.clean_session) 54 | end 55 | 56 | def test_23char_client_id 57 | fake_server do |fs| 58 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 59 | @cmd_result = run_cmd( 60 | 'mqtt-sn-pub', 61 | '-i' => 'ABCDEFGHIJKLMNOPQRSTUVW', 62 | '-T' => 10, 63 | '-m' => 'message', 64 | '-p' => fs.port, 65 | '-h' => fs.address 66 | ) 67 | end 68 | end 69 | 70 | assert_empty(@cmd_result) 71 | assert_equal('ABCDEFGHIJKLMNOPQRSTUVW', @packet.client_id) 72 | assert_equal(10, @packet.keep_alive) 73 | assert_equal(true, @packet.clean_session) 74 | end 75 | 76 | def test_connack_congestion 77 | fake_server do |fs| 78 | def fs.handle_connect(packet) 79 | MQTT::SN::Packet::Connack.new(:return_code => 0x01) 80 | end 81 | 82 | fs.wait_for_packet(MQTT::SN::Packet::Connect) do 83 | @cmd_result = run_cmd( 84 | 'mqtt-sn-pub', 85 | '-T' => 10, 86 | '-m' => 'message', 87 | '-p' => fs.port, 88 | '-h' => fs.address 89 | ) 90 | end 91 | end 92 | 93 | assert_match(/CONNECT error: Rejected: congestion/, @cmd_result[0]) 94 | end 95 | 96 | def test_no_connack 97 | fake_server do |fs| 98 | def fs.handle_connect(packet) 99 | MQTT::SN::Packet::Disconnect.new 100 | end 101 | 102 | fs.wait_for_packet(MQTT::SN::Packet::Connect) do 103 | @cmd_result = run_cmd( 104 | 'mqtt-sn-pub', 105 | '-T' => 10, 106 | '-m' => 'message', 107 | '-p' => fs.port, 108 | '-h' => fs.address 109 | ) 110 | end 111 | end 112 | 113 | assert_match(/ERROR Was expecting CONNACK packet but received: DISCONNECT/, @cmd_result[0]) 114 | end 115 | 116 | def test_too_long_client_id 117 | fake_server do |fs| 118 | @cmd_result = run_cmd( 119 | 'mqtt-sn-pub', 120 | '-i' => 'C' * 255, 121 | '-T' => 10, 122 | '-m' => 'message', 123 | '-p' => fs.port, 124 | '-h' => fs.address 125 | ) 126 | end 127 | 128 | assert_match(/ERROR Client id is too long/, @cmd_result[0]) 129 | end 130 | 131 | def test_publish_qos_n1 132 | fake_server do |fs| 133 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 134 | @cmd_result = run_cmd( 135 | 'mqtt-sn-pub', 136 | '-q' => -1, 137 | '-T' => 10, 138 | '-m' => 'test_publish_qos_n1', 139 | '-p' => fs.port, 140 | '-h' => fs.address 141 | ) 142 | end 143 | end 144 | 145 | assert_empty(@cmd_result) 146 | assert_equal(10, @packet.topic_id) 147 | assert_equal(:predefined, @packet.topic_id_type) 148 | assert_equal('test_publish_qos_n1', @packet.data) 149 | assert_equal(-1, @packet.qos) 150 | assert_equal(false, @packet.retain) 151 | assert_equal(0, @packet.id) 152 | end 153 | 154 | def test_publish_debug 155 | fake_server do |fs| 156 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 157 | @cmd_result = run_cmd( 158 | 'mqtt-sn-pub', 159 | ['-d', 160 | '-t', 'topic', 161 | '-m', 'test_publish_qos_0_debug', 162 | '-p', fs.port, 163 | '-h', fs.address] 164 | ) 165 | end 166 | end 167 | 168 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 1/, @cmd_result) 169 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) 170 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) 171 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) 172 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending PUBLISH packet/, @cmd_result) 173 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) 174 | end 175 | 176 | def test_publish_debug_2 177 | fake_server do |fs| 178 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 179 | @cmd_result = run_cmd( 180 | 'mqtt-sn-pub', 181 | ['-d', '-d', 182 | '-i', 'fixed_client_id', 183 | '-t', 'topic', 184 | '-m', 'test_publish_qos_0_debug', 185 | '-p', fs.port, 186 | '-h', fs.address] 187 | ) 188 | end 189 | end 190 | 191 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 2/, @cmd_result) 192 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) 193 | assert_includes_match(/Sending 21 bytes\. Type=CONNECT on Socket: 3/, @cmd_result) 194 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) 195 | assert_includes_match(/Received 3 bytes from 127.0.0.1\:\d+. Type=CONNACK on Socket/, @cmd_result) 196 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) 197 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending PUBLISH packet/, @cmd_result) 198 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) 199 | end 200 | 201 | def test_publish_from_file 202 | fake_server do |fs| 203 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 204 | @cmd_result = run_cmd( 205 | 'mqtt-sn-pub', 206 | ['-t', 'topic', 207 | '-f', 'test.txt', 208 | '-p', fs.port, 209 | '-h', fs.address] 210 | ) 211 | end 212 | end 213 | 214 | assert_empty(@cmd_result) 215 | assert_equal(1, @packet.topic_id) 216 | assert_equal(:normal, @packet.topic_id_type) 217 | assert_equal('The is the contents of test.txt', @packet.data) 218 | assert_equal(0, @packet.qos) 219 | end 220 | 221 | def test_publish_from_binary_file 222 | ## Test for a file that contains null bytes 223 | fake_server do |fs| 224 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 225 | @cmd_result = run_cmd( 226 | 'mqtt-sn-pub', 227 | ['-t', 'topic', 228 | '-f', 'test.bin', 229 | '-p', fs.port, 230 | '-h', fs.address] 231 | ) 232 | end 233 | end 234 | 235 | assert_empty(@cmd_result) 236 | assert_equal(1, @packet.topic_id) 237 | assert_equal(:normal, @packet.topic_id_type) 238 | assert_equal(12, @packet.data.length) 239 | assert_equal("\x01\x02\x03\x04\x05\x00Hello\x00", @packet.data) 240 | assert_equal(0, @packet.qos) 241 | end 242 | 243 | def test_publish_from_file_too_big 244 | fake_server do |fs| 245 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 246 | @cmd_result = run_cmd( 247 | 'mqtt-sn-pub', 248 | ['-t', 'topic', 249 | '-f', 'test_big.txt', 250 | '-p', fs.port, 251 | '-h', fs.address] 252 | ) 253 | end 254 | end 255 | 256 | assert_match(/WARN Input file is longer than the maximum message size/, @cmd_result[0]) 257 | 258 | assert_equal(1, @packet.topic_id) 259 | assert_equal(:normal, @packet.topic_id_type) 260 | assert_equal(248, @packet.data.length) 261 | assert_equal(0, @packet.qos) 262 | end 263 | 264 | def test_publish_from_file_hyphen 265 | fake_server do |fs| 266 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 267 | @cmd_result = run_cmd( 268 | 'mqtt-sn-pub', 269 | ['-t', 'topic', 270 | '-f', '-', 271 | '-p', fs.port, 272 | '-h', fs.address], 273 | 'Message from file -' 274 | ) 275 | end 276 | end 277 | 278 | assert_empty(@cmd_result) 279 | assert_equal('Message from file -', @packet.data) 280 | end 281 | 282 | def test_publish_from_stdin 283 | fake_server do |fs| 284 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 285 | @cmd_result = run_cmd( 286 | 'mqtt-sn-pub', 287 | ['-t', 'topic', 288 | '-s', 289 | '-p', fs.port, 290 | '-h', fs.address], 291 | 'Message from STDIN' 292 | ) 293 | end 294 | end 295 | 296 | assert_empty(@cmd_result) 297 | assert_equal('Message from STDIN', @packet.data) 298 | end 299 | 300 | def test_publish_multiline_from_stdin 301 | server = fake_server do |fs| 302 | fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 303 | @cmd_result = run_cmd( 304 | 'mqtt-sn-pub', 305 | ['-t', 'topic', 306 | '-l', 307 | '-p', fs.port, 308 | '-h', fs.address], 309 | "Message 1\nMessage 2\nMessage 3\n" 310 | ) 311 | end 312 | end 313 | 314 | publish_packets = server.packets_received.select do |packet| 315 | packet.is_a?(MQTT::SN::Packet::Publish) 316 | end 317 | 318 | assert_empty(@cmd_result) 319 | assert_equal(['Message 1', 'Message 2', 'Message 3'], publish_packets.map {|p| p.data}) 320 | assert_equal([1, 1, 1], publish_packets.map {|p| p.topic_id}) 321 | assert_equal([:normal, :normal, :normal], publish_packets.map {|p| p.topic_id_type}) 322 | assert_equal([0, 0, 0], publish_packets.map {|p| p.qos}) 323 | end 324 | 325 | def test_publish_multiline_from_stdin_no_newline 326 | server = fake_server do |fs| 327 | fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 328 | @cmd_result = run_cmd( 329 | 'mqtt-sn-pub', 330 | ['-t', 'topic', 331 | '-l', 332 | '-p', fs.port, 333 | '-h', fs.address], 334 | "Message 1\nMessage 2" 335 | ) 336 | end 337 | end 338 | 339 | publish_packets = server.packets_received.select do |packet| 340 | packet.is_a?(MQTT::SN::Packet::Publish) 341 | end 342 | 343 | assert_includes_match(/Failed to find newline when reading message/, @cmd_result) 344 | assert_equal(['Message 1'], publish_packets.map {|p| p.data}) 345 | assert_equal([1], publish_packets.map {|p| p.topic_id}) 346 | assert_equal([:normal], publish_packets.map {|p| p.topic_id_type}) 347 | assert_equal([0], publish_packets.map {|p| p.qos}) 348 | end 349 | 350 | def test_publish_qos_0 351 | fake_server do |fs| 352 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 353 | @cmd_result = run_cmd( 354 | 'mqtt-sn-pub', 355 | '-q' => 0, 356 | '-t' => 'topic', 357 | '-m' => 'test_publish_qos_0', 358 | '-p' => fs.port, 359 | '-h' => fs.address 360 | ) 361 | end 362 | end 363 | 364 | assert_empty(@cmd_result) 365 | assert_equal(1, @packet.topic_id) 366 | assert_equal(:normal, @packet.topic_id_type) 367 | assert_equal('test_publish_qos_0', @packet.data) 368 | assert_equal(0, @packet.qos) 369 | assert_equal(false, @packet.retain) 370 | assert_equal(0, @packet.id) 371 | end 372 | 373 | def test_publish_qos_0_short 374 | fake_server do |fs| 375 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 376 | @cmd_result = run_cmd( 377 | 'mqtt-sn-pub', 378 | '-q' => 0, 379 | '-t' => 'TT', 380 | '-m' => 'test_publish_qos_0_short', 381 | '-p' => fs.port, 382 | '-h' => fs.address 383 | ) 384 | end 385 | end 386 | 387 | assert_empty(@cmd_result) 388 | assert_equal('TT', @packet.topic_id) 389 | assert_equal(:short, @packet.topic_id_type) 390 | assert_equal('test_publish_qos_0_short', @packet.data) 391 | assert_equal(0, @packet.qos) 392 | assert_equal(false, @packet.retain) 393 | assert_equal(0, @packet.id) 394 | end 395 | 396 | def test_publish_qos_0_predefined 397 | fake_server do |fs| 398 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 399 | @cmd_result = run_cmd( 400 | 'mqtt-sn-pub', 401 | '-q' => 0, 402 | '-T' => 127, 403 | '-m' => 'test_publish_qos_0_predefined', 404 | '-p' => fs.port, 405 | '-h' => fs.address 406 | ) 407 | end 408 | end 409 | 410 | assert_empty(@cmd_result) 411 | assert_equal(127, @packet.topic_id) 412 | assert_equal(:predefined, @packet.topic_id_type) 413 | assert_equal('test_publish_qos_0_predefined', @packet.data) 414 | assert_equal(0, @packet.qos) 415 | assert_equal(false, @packet.retain) 416 | assert_equal(0, @packet.id) 417 | end 418 | 419 | def test_publish_qos_0_retained 420 | fake_server do |fs| 421 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 422 | @cmd_result = run_cmd( 423 | 'mqtt-sn-pub', 424 | ['-r', 425 | '-t', 'topic', 426 | '-m', 'test_publish_retained', 427 | '-p', fs.port, 428 | '-h', fs.address] 429 | ) 430 | end 431 | end 432 | 433 | assert_empty(@cmd_result) 434 | assert_equal(1, @packet.topic_id) 435 | assert_equal(:normal, @packet.topic_id_type) 436 | assert_equal('test_publish_retained', @packet.data) 437 | assert_equal(0, @packet.qos) 438 | assert_equal(true, @packet.retain) 439 | assert_equal(0, @packet.id) 440 | end 441 | 442 | def test_publish_qos_0_empty 443 | fake_server do |fs| 444 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 445 | @cmd_result = run_cmd( 446 | 'mqtt-sn-pub', 447 | ['-r', '-n', 448 | '-t', 'topic', 449 | '-p', fs.port, 450 | '-h', fs.address] 451 | ) 452 | end 453 | end 454 | 455 | assert_empty(@cmd_result) 456 | assert_equal(1, @packet.topic_id) 457 | assert_equal(:normal, @packet.topic_id_type) 458 | assert_equal('', @packet.data) 459 | assert_equal(0, @packet.qos) 460 | assert_equal(true, @packet.retain) 461 | assert_equal(0, @packet.id) 462 | end 463 | 464 | def test_publish_qos_1 465 | fake_server do |fs| 466 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 467 | @cmd_result = run_cmd( 468 | 'mqtt-sn-pub', 469 | '-q' => 1, 470 | '-t' => 'topic', 471 | '-m' => 'test_publish_qos_1', 472 | '-p' => fs.port, 473 | '-h' => fs.address 474 | ) 475 | end 476 | end 477 | 478 | assert_empty(@cmd_result) 479 | assert_equal(1, @packet.topic_id) 480 | assert_equal(:normal, @packet.topic_id_type) 481 | assert_equal('test_publish_qos_1', @packet.data) 482 | assert_equal(1, @packet.qos) 483 | assert_equal(false, @packet.retain) 484 | assert_equal(2, @packet.id) # REGISTER for topic ID has id 1 485 | end 486 | 487 | def test_publish_qos_1_puback_timeout 488 | fake_server do |fs| 489 | def fs.handle_publish(packet) 490 | nil 491 | end 492 | 493 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 494 | @cmd_result = run_cmd( 495 | 'mqtt-sn-pub', 496 | '-q' => 1, 497 | '-d' => '', 498 | '-k' => 2, 499 | '-t' => 'topic', 500 | '-m' => 'test_publish_qos_1', 501 | '-p' => fs.port, 502 | '-h' => fs.address 503 | ) 504 | end 505 | end 506 | 507 | assert_includes_match(/Timed out while waiting for a PUBACK from gateway/, @cmd_result) 508 | end 509 | 510 | def test_publish_ipv6 511 | unless have_ipv6? 512 | skip("IPv6 is not available on this system") 513 | end 514 | 515 | server = fake_server(nil, '::1') do |fs| 516 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Publish) do 517 | @cmd_result = run_cmd( 518 | 'mqtt-sn-pub', 519 | ['-d', '-d', 520 | '-t', 'topic', 521 | '-m', 'test', 522 | '-p', fs.port, 523 | '-h', fs.address] 524 | ) 525 | end 526 | end 527 | 528 | assert_includes_match(/Received 3 bytes from ::1:#{server.port}/, @cmd_result) 529 | assert_equal('test', @packet.data) 530 | end 531 | 532 | def test_invalid_qos 533 | @cmd_result = run_cmd( 534 | 'mqtt-sn-pub', 535 | '-q' => '2', 536 | '-t' => 'topic', 537 | '-m' => 'message' 538 | ) 539 | assert_match(/Only QoS level 0, 1 or -1 is supported/, @cmd_result[0]) 540 | end 541 | 542 | def test_payload_too_big 543 | fake_server do |fs| 544 | @cmd_result = run_cmd( 545 | 'mqtt-sn-pub', 546 | ['-t', 'topic', 547 | '-m', 'm' * 255, 548 | '-p', fs.port, 549 | '-h', fs.address] 550 | ) 551 | end 552 | assert_match(/Payload is too big/, @cmd_result[0]) 553 | end 554 | 555 | def test_both_topic_name_and_id 556 | @cmd_result = run_cmd( 557 | 'mqtt-sn-pub', 558 | '-t' => 'topic_name', 559 | '-T' => 10, 560 | '-m' => 'message' 561 | ) 562 | assert_match(/Please provide either a topic id or a topic name, not both/, @cmd_result[0]) 563 | end 564 | 565 | def test_both_message_and_file 566 | @cmd_result = run_cmd( 567 | 'mqtt-sn-pub', 568 | '-t' => 'topic_name', 569 | '-m' => 'message', 570 | '-f' => '/dev/zero' 571 | ) 572 | assert_match(/Please provide either message data or a message file, not both/, @cmd_result[0]) 573 | end 574 | 575 | def test_file_doesnt_exist 576 | fake_server do |fs| 577 | @cmd_result = run_cmd( 578 | 'mqtt-sn-pub', 579 | '-t' => 'topic_name', 580 | '-f' => '/doesnt/exist', 581 | '-p' => fs.port, 582 | '-h' => fs.address 583 | ) do |cmd| 584 | wait_for_output_then_kill(cmd) 585 | end 586 | end 587 | assert_match(/Failed to open message file/, @cmd_result[0]) 588 | end 589 | 590 | def test_both_qos_n1_topic_name 591 | @cmd_result = run_cmd( 592 | 'mqtt-sn-pub', 593 | '-q' => -1, 594 | '-t' => 'topic_name', 595 | '-m' => 'message' 596 | ) 597 | assert_match(/Either a pre-defined topic id or a short topic name must be given for QoS -1/, @cmd_result[0]) 598 | end 599 | 600 | def test_topic_name_too_long 601 | fake_server do |fs| 602 | @cmd_result = run_cmd( 603 | 'mqtt-sn-pub', 604 | ['-t', 'x' * 255, 605 | '-m', 'message', 606 | '-p', fs.port, 607 | '-h', fs.address] 608 | ) do |cmd| 609 | wait_for_output_then_kill(cmd) 610 | end 611 | end 612 | assert_match(/ERROR Topic name is too long/, @cmd_result[0]) 613 | end 614 | 615 | def test_register_invalid_topic_name 616 | fake_server do |fs| 617 | def fs.handle_register(packet) 618 | MQTT::SN::Packet::Regack.new( 619 | :id => packet.id, 620 | :return_code => 2 621 | ) 622 | end 623 | 624 | @cmd_result = run_cmd( 625 | 'mqtt-sn-pub', 626 | ['-t', '/!invalid%topic"name', 627 | '-m', 'message', 628 | '-p', fs.port, 629 | '-h', fs.address] 630 | ) do |cmd| 631 | wait_for_output_then_kill(cmd) 632 | end 633 | end 634 | 635 | assert_match(/ERROR REGISTER failed: Rejected: invalid topic ID/, @cmd_result[0]) 636 | end 637 | 638 | def test_connect_fail 639 | skip("Unable to get this test to run reliably") 640 | # @cmd_result = run_cmd( 641 | # 'mqtt-sn-pub', 642 | # ['-t', 'topic', 643 | # '-m', 'message', 644 | # '-h', '0.0.0.1', 645 | # '-p', '29567'] 646 | # ) do |cmd| 647 | # wait_for_output_then_kill(cmd) 648 | # end 649 | # 650 | # assert_match(/ERROR Could not connect to remote host/, @cmd_result[0]) 651 | end 652 | 653 | def test_hostname_lookup_fail 654 | @cmd_result = run_cmd( 655 | 'mqtt-sn-pub', 656 | ['-t', 'topic', 657 | '-m', 'message', 658 | '-p', '29567', 659 | '-h', '!(invalid)'] 660 | ) do |cmd| 661 | wait_for_output_then_kill(cmd) 662 | end 663 | 664 | assert_match(/nodename nor servname provided, or not known|Name or service not known/, @cmd_result[0]) 665 | end 666 | 667 | def test_disconnect_after_publish 668 | fake_server do |fs| 669 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 670 | @cmd_result = run_cmd( 671 | 'mqtt-sn-pub', 672 | '-T' => 10, 673 | '-m' => 'message', 674 | '-p' => fs.port, 675 | '-h' => fs.address 676 | ) 677 | end 678 | end 679 | 680 | assert_empty(@cmd_result) 681 | assert_kind_of(MQTT::SN::Packet::Disconnect, @packet) 682 | assert_nil(@packet.duration) 683 | end 684 | 685 | def test_disconnect_after_publish_with_sleep 686 | fake_server do |fs| 687 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 688 | @cmd_result = run_cmd( 689 | 'mqtt-sn-pub', 690 | '-e' => 3600, 691 | '-T' => 10, 692 | '-m' => 'message', 693 | '-p' => fs.port, 694 | '-h' => fs.address 695 | ) 696 | end 697 | end 698 | 699 | assert_empty(@cmd_result) 700 | assert_equal(MQTT::SN::Packet::Disconnect, @packet.class) 701 | assert_equal(3600, @packet.duration) 702 | end 703 | 704 | def test_disconnect_duration_warning 705 | fake_server do |fs| 706 | def fs.handle_disconnect(packet) 707 | MQTT::SN::Packet::Disconnect.new( 708 | :duration => 10 709 | ) 710 | end 711 | 712 | @cmd_result = run_cmd( 713 | 'mqtt-sn-pub', 714 | '-T' => 10, 715 | '-m' => 'message', 716 | '-p' => fs.port, 717 | '-h' => fs.address 718 | ) 719 | end 720 | 721 | assert_match(/DISCONNECT warning. Gateway returned duration in disconnect packet/, @cmd_result[0]) 722 | end 723 | 724 | end 725 | -------------------------------------------------------------------------------- /test/mqtt-sn-serial-bridge-test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'test_helper' 4 | 5 | class MqttSnSerialBridgeTest < Minitest::Test 6 | 7 | def test_usage 8 | @cmd_result = run_cmd('mqtt-sn-serial-bridge', '-?') 9 | assert_match(/^Usage: mqtt-sn-serial-bridge/, @cmd_result[0]) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /test/mqtt-sn-sub-test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'test_helper' 4 | 5 | class MqttSnSubTest < Minitest::Test 6 | 7 | def test_usage 8 | @cmd_result = run_cmd('mqtt-sn-sub', '-?') 9 | assert_match(/^Usage: mqtt-sn-sub/, @cmd_result[0]) 10 | end 11 | 12 | def test_no_arguments 13 | @cmd_result = run_cmd('mqtt-sn-sub') 14 | assert_match(/^Usage: mqtt-sn-sub/, @cmd_result[0]) 15 | end 16 | 17 | def test_default_client_id 18 | fake_server do |fs| 19 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 20 | @cmd_result = run_cmd( 21 | 'mqtt-sn-sub', 22 | ['-1', 23 | '-t', 'test', 24 | '-p', fs.port, 25 | '-h', fs.address] 26 | ) 27 | end 28 | end 29 | 30 | assert_equal(["Message for test"], @cmd_result) 31 | assert_match(/^mqtt-sn-tools-(\d+)$/, @packet.client_id) 32 | assert_equal(10, @packet.keep_alive) 33 | assert_equal(true, @packet.clean_session) 34 | end 35 | 36 | def test_custom_client_id 37 | fake_server do |fs| 38 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 39 | @cmd_result = run_cmd( 40 | 'mqtt-sn-sub', 41 | ['-1', 42 | '-i', 'test_custom_client_id', 43 | '-t', 'test', 44 | '-p', fs.port, 45 | '-h', fs.address] 46 | ) 47 | end 48 | end 49 | 50 | assert_equal(["Message for test"], @cmd_result) 51 | assert_equal('test_custom_client_id', @packet.client_id) 52 | assert_equal(10, @packet.keep_alive) 53 | assert_equal(true, @packet.clean_session) 54 | end 55 | 56 | def test_custom_keep_alive 57 | fake_server do |fs| 58 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) do 59 | @cmd_result = run_cmd( 60 | 'mqtt-sn-sub', 61 | ['-1', 62 | '-k', 5, 63 | '-t', 'test', 64 | '-p', fs.port, 65 | '-h', fs.address] 66 | ) 67 | end 68 | end 69 | 70 | assert_equal(["Message for test"], @cmd_result) 71 | assert_match(/^mqtt-sn-tools/, @packet.client_id) 72 | assert_equal(5, @packet.keep_alive) 73 | assert_equal(true, @packet.clean_session) 74 | end 75 | 76 | def test_connack_timeout 77 | fake_server do |fs| 78 | def fs.handle_connect(packet) 79 | nil 80 | end 81 | 82 | fs.wait_for_packet(MQTT::SN::Packet::Connect) do 83 | @cmd_result = run_cmd( 84 | 'mqtt-sn-sub', 85 | ['-1', 86 | '-d', 87 | '-k', 2, 88 | '-t', 'test', 89 | '-p', fs.port, 90 | '-h', fs.address] 91 | ) 92 | end 93 | end 94 | 95 | assert_includes_match(/Timed out waiting for packet/, @cmd_result) 96 | end 97 | 98 | def test_subscribe_one 99 | fake_server do |fs| 100 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 101 | @cmd_result = run_cmd( 102 | 'mqtt-sn-sub', 103 | ['-1', 104 | '-t', 'test', 105 | '-p', fs.port, 106 | '-h', fs.address] 107 | ) 108 | end 109 | end 110 | 111 | assert_equal(["Message for test"], @cmd_result) 112 | assert_equal('test', @packet.topic_name) 113 | assert_equal(:normal, @packet.topic_id_type) 114 | assert_equal(0, @packet.qos) 115 | end 116 | 117 | def test_subscribe_one_debug 118 | fake_server do |fs| 119 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 120 | @cmd_result = run_cmd( 121 | 'mqtt-sn-sub', 122 | ['-d', '-1', 123 | '-t', 'test', 124 | '-p', fs.port, 125 | '-h', fs.address] 126 | ) 127 | end 128 | end 129 | 130 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Debug level is: 1/, @cmd_result) 131 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending CONNECT packet/, @cmd_result) 132 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG waiting for packet/, @cmd_result) 133 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG CONNACK return code: 0x00/, @cmd_result) 134 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending SUBSCRIBE packet/, @cmd_result) 135 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG SUBACK return code: 0x00/, @cmd_result) 136 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Sending DISCONNECT packet/, @cmd_result) 137 | end 138 | 139 | def test_subscribe_one_verbose 140 | fake_server do |fs| 141 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 142 | @cmd_result = run_cmd( 143 | 'mqtt-sn-sub', 144 | ['-1', '-v', 145 | '-t', 'test', 146 | '-p', fs.port, 147 | '-h', fs.address] 148 | ) 149 | end 150 | end 151 | 152 | assert_equal(["test: Message for test"], @cmd_result) 153 | assert_equal('test', @packet.topic_name) 154 | assert_equal(:normal, @packet.topic_id_type) 155 | assert_equal(0, @packet.qos) 156 | end 157 | 158 | def test_subscribe_one_verbose_time 159 | fake_server do |fs| 160 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 161 | @cmd_result = run_cmd( 162 | 'mqtt-sn-sub', 163 | ['-1', '-V', 164 | '-t', 'test', 165 | '-p', fs.port, 166 | '-h', fs.address] 167 | ) 168 | end 169 | end 170 | 171 | assert_equal(1, @cmd_result.count) 172 | assert_match(/\d{4}\-\d{2}\-\d{2} \d{2}\:\d{2}\:\d{2} test: Message for test/, @cmd_result[0]) 173 | assert_equal('test', @packet.topic_name) 174 | assert_equal(:normal, @packet.topic_id_type) 175 | assert_equal(0, @packet.qos) 176 | end 177 | 178 | def test_subscribe_one_short 179 | fake_server do |fs| 180 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 181 | @cmd_result = run_cmd( 182 | 'mqtt-sn-sub', 183 | ['-1', '-v', 184 | '-t', 'tt', 185 | '-p', fs.port, 186 | '-h', fs.address] 187 | ) 188 | end 189 | end 190 | 191 | assert_equal(["tt: Message for tt"], @cmd_result) 192 | assert_equal('tt', @packet.topic_name) 193 | assert_equal(:short, @packet.topic_id_type) 194 | assert_equal(0, @packet.qos) 195 | end 196 | 197 | def test_subscribe_one_predefined 198 | fake_server do |fs| 199 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) do 200 | @cmd_result = run_cmd( 201 | 'mqtt-sn-sub', 202 | ['-1', '-v', 203 | '-T', 17, 204 | '-p', fs.port, 205 | '-h', fs.address] 206 | ) 207 | end 208 | end 209 | 210 | assert_equal(["0011: Message for #17"], @cmd_result) 211 | assert_nil(@packet.topic_name) 212 | assert_equal(17, @packet.topic_id) 213 | assert_equal(:predefined, @packet.topic_id_type) 214 | assert_equal(0, @packet.qos) 215 | end 216 | 217 | def test_subscribe_then_int 218 | fake_server do |fs| 219 | @cmd_result = run_cmd( 220 | 'mqtt-sn-sub', 221 | ['-v', 222 | '-t', 'test', 223 | '-p', fs.port, 224 | '-h', fs.address] 225 | ) do |cmd| 226 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 227 | wait_for_output_then_kill(cmd, 'INT') 228 | end 229 | end 230 | 231 | assert_equal(["test: Message for test"], @cmd_result) 232 | assert_equal('test', @packet.topic_name) 233 | assert_equal(:normal, @packet.topic_id_type) 234 | assert_equal(0, @packet.qos) 235 | end 236 | 237 | def test_subscribe_then_term 238 | fake_server do |fs| 239 | @cmd_result = run_cmd( 240 | 'mqtt-sn-sub', 241 | ['-v', 242 | '-t', 'test', 243 | '-p', fs.port, 244 | '-h', fs.address] 245 | ) do |cmd| 246 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 247 | wait_for_output_then_kill(cmd, 'TERM') 248 | end 249 | end 250 | 251 | assert_equal(["test: Message for test"], @cmd_result) 252 | assert_equal('test', @packet.topic_name) 253 | assert_equal(:normal, @packet.topic_id_type) 254 | assert_equal(0, @packet.qos) 255 | end 256 | 257 | def test_subscribe_then_hup 258 | fake_server do |fs| 259 | @cmd_result = run_cmd( 260 | 'mqtt-sn-sub', 261 | ['-v', 262 | '-t', 'test', 263 | '-p', fs.port, 264 | '-h', fs.address] 265 | ) do |cmd| 266 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 267 | wait_for_output_then_kill(cmd, 'HUP') 268 | end 269 | end 270 | 271 | assert_equal(["test: Message for test"], @cmd_result) 272 | assert_equal('test', @packet.topic_name) 273 | assert_equal(:normal, @packet.topic_id_type) 274 | assert_equal(0, @packet.qos) 275 | end 276 | 277 | def test_subscribe_two_topic_names 278 | fake_server do |fs| 279 | @cmd_result = run_cmd( 280 | 'mqtt-sn-sub', 281 | ['-v', 282 | '-q', 1, 283 | '-t', 'test1', 284 | '-t', 'test2', 285 | '-p', fs.port, 286 | '-h', fs.address] 287 | ) do |cmd| 288 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) 289 | wait_for_output_then_kill(cmd, 'INT') 290 | end 291 | end 292 | 293 | assert_equal(2, @cmd_result.length) 294 | assert_equal("test1: Message for test1", @cmd_result[0]) 295 | assert_equal("test2: Message for test2", @cmd_result[1]) 296 | end 297 | 298 | def test_subscribe_multiple_topics 299 | fake_server do |fs| 300 | @cmd_result = run_cmd( 301 | 'mqtt-sn-sub', 302 | ['-v', 303 | '-q', 1, 304 | '-t', 'name', 305 | '-t', 'TT', 306 | '-T', 0x10, 307 | '-p', fs.port, 308 | '-h', fs.address] 309 | ) do |cmd| 310 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) 311 | wait_for_output_then_kill(cmd, 'INT') 312 | end 313 | end 314 | 315 | assert_equal(3, @cmd_result.length) 316 | assert_equal("name: Message for name", @cmd_result[0]) 317 | assert_equal("TT: Message for TT", @cmd_result[1]) 318 | assert_equal("0010: Message for #16", @cmd_result[2]) 319 | end 320 | 321 | def test_subscribe_thirty_topics 322 | topics = (1..30).map { |t| ['-t', "topic#{t}"] } 323 | 324 | fake_server do |fs| 325 | @cmd_result = run_cmd( 326 | 'mqtt-sn-sub', 327 | ['-v', 328 | '-q', 1, 329 | topics, 330 | '-p', fs.port, 331 | '-h', fs.address].compact 332 | ) do |cmd| 333 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) 334 | wait_for_output_then_kill(cmd, 'INT') 335 | end 336 | end 337 | 338 | assert_equal(30, @cmd_result.length) 339 | assert_equal("topic1: Message for topic1", @cmd_result[0]) 340 | assert_equal("topic30: Message for topic30", @cmd_result[29]) 341 | end 342 | 343 | def test_subscribe_invalid_topic_id 344 | fake_server do |fs| 345 | def fs.handle_subscribe(packet) 346 | MQTT::SN::Packet::Suback.new( 347 | :id => packet.id, 348 | :topic_id => 0, 349 | :topic_id_type => packet.topic_id_type, 350 | :return_code => 0x02 351 | ) 352 | end 353 | 354 | @cmd_result = run_cmd( 355 | 'mqtt-sn-sub', 356 | ['-v', 357 | '-T', 123, 358 | '-p', fs.port, 359 | '-h', fs.address] 360 | ) do |cmd| 361 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 362 | wait_for_output_then_kill(cmd, 'HUP') 363 | end 364 | end 365 | 366 | assert_includes_match(/ERROR SUBSCRIBE error: Rejected: invalid topic ID/, @cmd_result) 367 | end 368 | 369 | def test_subscribe_one_qos1 370 | fake_server do |fs| 371 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Puback) do 372 | @cmd_result = run_cmd( 373 | 'mqtt-sn-sub', 374 | ['-1', 375 | '-q', 1, 376 | '-t', 'test', 377 | '-p', fs.port, 378 | '-h', fs.address] 379 | ) 380 | end 381 | end 382 | 383 | assert_equal(["Message for test"], @cmd_result) 384 | assert_equal(1, @packet.topic_id) 385 | assert_equal(0, @packet.id) 386 | end 387 | 388 | def test_subscribe_invalid_connack_packet 389 | fake_server do |fs| 390 | def fs.handle_connect(packet) 391 | "\x00\x05\x00" 392 | end 393 | 394 | @cmd_result = run_cmd( 395 | 'mqtt-sn-sub', 396 | ['-v', 397 | '-T', 123, 398 | '-p', fs.port, 399 | '-h', fs.address] 400 | ) do |cmd| 401 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 402 | wait_for_output_then_kill(cmd) 403 | end 404 | end 405 | 406 | assert_includes_match(/Packet length header is not valid/, @cmd_result) 407 | end 408 | 409 | def test_subscribe_incorrect_connack_packet_length 410 | fake_server do |fs| 411 | def fs.handle_connect(packet) 412 | "\x04\x05\x00" 413 | end 414 | 415 | @cmd_result = run_cmd( 416 | 'mqtt-sn-sub', 417 | ['-v', 418 | '-T', 123, 419 | '-p', fs.port, 420 | '-h', fs.address] 421 | ) do |cmd| 422 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 423 | wait_for_output_then_kill(cmd) 424 | end 425 | end 426 | 427 | assert_includes_match(/Read 3 bytes but packet length is 4 bytes/, @cmd_result) 428 | end 429 | 430 | def test_subscribe_id_mismatch 431 | fake_server do |fs| 432 | def fs.handle_subscribe(packet) 433 | MQTT::SN::Packet::Suback.new( 434 | :id => 222, 435 | :topic_id => 0, 436 | :topic_id_type => packet.topic_id_type, 437 | :return_code => 0x00 438 | ) 439 | end 440 | 441 | @cmd_result = run_cmd( 442 | 'mqtt-sn-sub', 443 | ['-v', '-d', 444 | '-t', 'test', 445 | '-p', fs.port, 446 | '-h', fs.address] 447 | ) do |cmd| 448 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 449 | wait_for_output_then_kill(cmd) 450 | end 451 | end 452 | 453 | assert_includes_match(/WARN Message id in SUBACK does not equal message id sent/, @cmd_result) 454 | assert_includes_match(/DEBUG Expecting: 1/, @cmd_result) 455 | assert_includes_match(/DEBUG Actual: 222/, @cmd_result) 456 | end 457 | 458 | def test_subscribe_then_interupt_debug 459 | fake_server do |fs| 460 | @cmd_result = run_cmd( 461 | 'mqtt-sn-sub', 462 | ['-v', '-d', 463 | '-t', 'test', 464 | '-p', fs.port, 465 | '-h', fs.address] 466 | ) do |cmd| 467 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 468 | wait_for_output_then_kill(cmd) 469 | end 470 | end 471 | 472 | assert_includes_match(/DEBUG Debug level is: 1/, @cmd_result) 473 | assert_includes_match(/DEBUG Got interrupt signal/, @cmd_result) 474 | assert_includes_match(/^test: Message for test$/, @cmd_result) 475 | assert_equal('test', @packet.topic_name) 476 | assert_equal(:normal, @packet.topic_id_type) 477 | assert_equal(0, @packet.qos) 478 | end 479 | 480 | def test_subscribe_no_clean_session 481 | @fs = fake_server do |fs| 482 | def fs.handle_connect(packet) 483 | [ 484 | MQTT::SN::Packet::Connack.new(:return_code => 0x00), 485 | MQTT::SN::Packet::Register.new(:topic_id => 5, :topic_name => 'old_topic'), 486 | MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'old_msg') 487 | ] 488 | end 489 | 490 | @cmd_result = run_cmd( 491 | 'mqtt-sn-sub', 492 | ['-c', '-v', 493 | '-t', 'test', 494 | '-p', fs.port, 495 | '-h', fs.address] 496 | ) do |cmd| 497 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 498 | wait_for_output_then_kill(cmd) 499 | end 500 | end 501 | 502 | assert_equal(["old_topic: old_msg", "test: Message for test"], @cmd_result) 503 | assert_match(/^mqtt-sn-tools/, @packet.client_id) 504 | assert_equal(10, @packet.keep_alive) 505 | assert_equal(false, @packet.clean_session) 506 | end 507 | 508 | def test_register_invalid_topic_id 509 | @fs = fake_server do |fs| 510 | def fs.handle_connect(packet) 511 | [ 512 | MQTT::SN::Packet::Connack.new(:return_code => 0x00), 513 | MQTT::SN::Packet::Register.new(:topic_id => 0, :topic_name => 'old_topic') 514 | ] 515 | end 516 | 517 | @cmd_result = run_cmd( 518 | 'mqtt-sn-sub', 519 | ['-c', '-v', 520 | '-t', 'test', 521 | '-p', fs.port, 522 | '-h', fs.address] 523 | ) do |cmd| 524 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 525 | wait_for_output_then_kill(cmd) 526 | end 527 | end 528 | 529 | assert_match(/ERROR Attempted to register invalid topic id: 0x0000/, @cmd_result[0]) 530 | assert_match(/test: Message for test/, @cmd_result[1]) 531 | end 532 | 533 | def test_register_invalid_topic_name 534 | @fs = fake_server do |fs| 535 | def fs.handle_connect(packet) 536 | [ 537 | MQTT::SN::Packet::Connack.new(:return_code => 0x00), 538 | MQTT::SN::Packet::Register.new(:topic_id => 5, :topic_name => ''), 539 | MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'old_msg') 540 | ] 541 | end 542 | 543 | @cmd_result = run_cmd( 544 | 'mqtt-sn-sub', 545 | ['-c', '-v', 546 | '-t', 'test', 547 | '-p', fs.port, 548 | '-h', fs.address] 549 | ) do |cmd| 550 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 551 | wait_for_output_then_kill(cmd) 552 | end 553 | end 554 | 555 | assert_match(/ERROR Attempted to register invalid topic name/, @cmd_result[0]) 556 | assert_match(/WARN Failed to lookup topic id: 0x0005/, @cmd_result[1]) 557 | assert_match(/test: Message for test/, @cmd_result[2]) 558 | end 559 | 560 | def test_recieve_non_registered_topic_id 561 | @fs = fake_server do |fs| 562 | def fs.handle_connect(packet) 563 | [ 564 | MQTT::SN::Packet::Connack.new(:return_code => 0x00), 565 | MQTT::SN::Packet::Publish.new(:topic_id => 5, :data => 'not registered') 566 | ] 567 | end 568 | 569 | @cmd_result = run_cmd( 570 | 'mqtt-sn-sub', 571 | ['-c', '-v', 572 | '-t', 'test', 573 | '-p', fs.port, 574 | '-h', fs.address] 575 | ) do |cmd| 576 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Connect) 577 | wait_for_output_then_kill(cmd) 578 | end 579 | end 580 | 581 | assert_match(/Failed to lookup topic id: 0x0005/, @cmd_result[0]) 582 | assert_match(/test: Message for test/, @cmd_result[1]) 583 | end 584 | 585 | def test_packet_too_long 586 | fake_server do |fs| 587 | def fs.handle_subscribe(packet) 588 | super(packet, 'x' * 256) 589 | end 590 | 591 | @cmd_result = run_cmd( 592 | 'mqtt-sn-sub', 593 | ['-v', '-d', 594 | '-t', 'test', 595 | '-p', fs.port, 596 | '-h', fs.address] 597 | ) do |cmd| 598 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Subscribe) 599 | wait_for_output_then_kill(cmd) 600 | end 601 | end 602 | 603 | assert_includes_match(/[\d\-]+ [\d\:]+ DEBUG Received 265 bytes from/, @cmd_result) 604 | assert_includes_match(/[\d\-]+ [\d\:]+ WARN Packet received is longer than this tool can handle/, @cmd_result) 605 | end 606 | 607 | def test_disconnect_after_recieve 608 | fake_server do |fs| 609 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 610 | @cmd_result = run_cmd( 611 | 'mqtt-sn-sub', 612 | ['-1', 613 | '-t', 'test', 614 | '-p', fs.port, 615 | '-h', fs.address] 616 | ) 617 | end 618 | end 619 | 620 | assert_equal(["Message for test"], @cmd_result) 621 | assert_kind_of(MQTT::SN::Packet::Disconnect, @packet) 622 | assert_nil(@packet.duration) 623 | end 624 | 625 | def test_disconnect_after_recieve_with_sleep 626 | fake_server do |fs| 627 | @packet = fs.wait_for_packet(MQTT::SN::Packet::Disconnect) do 628 | @cmd_result = run_cmd( 629 | 'mqtt-sn-sub', 630 | ['-1', 631 | '-e', 3600, 632 | '-t', 'test', 633 | '-p', fs.port, 634 | '-h', fs.address] 635 | ) 636 | end 637 | end 638 | 639 | assert_equal(["Message for test"], @cmd_result) 640 | assert_equal(MQTT::SN::Packet::Disconnect, @packet.class) 641 | assert_equal(3600, @packet.duration) 642 | end 643 | end 644 | -------------------------------------------------------------------------------- /test/test.bin: -------------------------------------------------------------------------------- 1 | Hello -------------------------------------------------------------------------------- /test/test.txt: -------------------------------------------------------------------------------- 1 | The is the contents of test.txt -------------------------------------------------------------------------------- /test/test_big.txt: -------------------------------------------------------------------------------- 1 | 00000000 f3 a7 4f ee 23 58 28 e8 48 df bc 96 42 6a 45 e6 |..O.#X(.H...BjE.| 2 | 00000010 8e ca 44 51 7d 3a 08 80 1f b8 58 fc 7c ca e6 37 |..DQ}:....X.|..7| 3 | 00000020 ac 71 28 aa d8 a7 b8 77 76 dc 5f 80 f0 c9 1e 46 |.q(....wv._....F| 4 | 00000030 5b 5b 89 5e 6e a5 22 f1 13 cd 1f ba 1a 5f 5b 5e |[[.^n."......_[^| 5 | 00000040 73 34 90 b3 ad c7 72 61 24 32 bb c1 21 f4 a7 8a |s4....ra$2..!...| 6 | 00000050 7f 15 68 d7 61 9c 68 fd 80 ef 48 ba 25 a6 8b f3 |..h.a.h...H.%...| 7 | 00000060 5d a9 ea 86 ac da 7b 21 82 be a7 3d 2c f7 f4 dd |].....{!...=,...| 8 | 00000070 52 79 53 4c e6 6f 35 32 80 92 92 7a 48 f8 98 ca |RySL.o52...zH...| 9 | 00000080 a2 79 df f5 4b c6 7c 43 5b c2 da f3 10 2e da 53 |.y..K.|C[......S| 10 | 00000090 09 fb 96 bb 9f a7 48 8e 27 c1 1d 51 32 59 a3 2e |......H.'..Q2Y..| 11 | 000000a0 5c de 4a 4d d2 8d a8 d7 6b 00 8d 63 a9 3d d1 1c |\.JM....k..c.=..| 12 | 000000b0 73 78 db 94 94 82 f6 f6 2e e0 16 ca e9 52 2b 89 |sx...........R+.| 13 | 000000c0 b0 8d 21 22 e1 a7 2a 26 12 9a c5 97 25 da c9 17 |..!"..*&....%...| 14 | 000000d0 ad 96 55 f2 a1 75 0d 8a be ed 13 41 51 74 c2 10 |..U..u.....AQt..| 15 | 000000e0 2e 2e cf 49 26 00 36 0a c4 ce 17 46 10 06 e9 f9 |...I&.6....F....| 16 | 000000f0 ad 6b 44 4e 82 8c 8c 79 fc 27 26 06 f9 f1 24 39 |.kDN...y.'&...$9| 17 | 00000100 e0 62 3a 3a 7d d0 78 cc 95 24 de f9 c2 b3 16 60 |.b::}.x..$.....`| 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__),'..','lib')) 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | Bundler.require(:default) 6 | require 'minitest/autorun' 7 | require 'fake_server' 8 | 9 | CMD_DIR = File.realpath('../..', __FILE__) 10 | 11 | def run_cmd(name, args=[], data=nil) 12 | args = [args] unless args.respond_to?(:flatten) 13 | cmd = [CMD_DIR + '/' + name] + args.flatten.map {|i| i.to_s} 14 | IO.popen([*cmd, :err => [:child, :out]], 'r+b') do |io| 15 | if block_given? 16 | yield(io) 17 | end 18 | unless data.nil? 19 | io.write data 20 | end 21 | io.close_write 22 | io.readlines.map {|line| line.strip} 23 | end 24 | end 25 | 26 | def wait_for_output_then_kill(io, signal='INT', timeout=0.5) 27 | IO.select([io], nil, nil, timeout) 28 | sleep(0.1) 29 | Process.kill(signal, io.pid) 30 | end 31 | 32 | def random_port 33 | 10000 + ((rand(10000) + Time.now.to_i) % 10000) 34 | end 35 | 36 | def fake_server(*args) 37 | fs = MQTT::SN::FakeServer.new(*args) 38 | fs.logger.level = Logger::WARN 39 | fs.start 40 | fs.wait_for_port_number 41 | yield(fs) 42 | fs.stop 43 | return fs 44 | end 45 | 46 | def have_ipv6? 47 | Socket.ip_address_list.any? { |addr| addr.ipv6? } 48 | end 49 | 50 | module Minitest::Assertions 51 | def assert_includes_match(regexp, array, msg=nil) 52 | msg = message(msg) { "Expected #{mu_pp(array)} to match #{mu_pp(regexp)}" } 53 | array.each do |item| 54 | if item =~ regexp 55 | assert true, msg 56 | return 57 | end 58 | end 59 | assert false, msg 60 | end 61 | end 62 | --------------------------------------------------------------------------------