├── .gitignore ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitmodules ├── Sdk.zig ├── README.md ├── LICENCE └── src └── MqttClient.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MasterQ32 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/mqtt-c"] 2 | path = vendor/mqtt-c 3 | url = https://github.com/LiamBindle/MQTT-C 4 | -------------------------------------------------------------------------------- /Sdk.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn sdkRoot() []const u8 { 4 | return std.fs.path.dirname(@src().file) orelse "."; 5 | } 6 | const sdk_root = sdkRoot(); 7 | 8 | pub const include_path = sdk_root ++ "/vendor/mqtt-c/include"; 9 | 10 | pub fn createLibrary(b: *std.build.Builder) *std.build.LibExeObjStep { 11 | const lib = b.addStaticLibrary("mqtt", null); 12 | lib.addCSourceFiles(&mqtt_c_sources, &.{}); 13 | lib.addIncludePath(include_path); 14 | lib.linkLibC(); 15 | return lib; 16 | } 17 | 18 | const mqtt_c_sources = [_][]const u8{ 19 | sdk_root ++ "/vendor/mqtt-c/src/mqtt.c", 20 | sdk_root ++ "/vendor/mqtt-c/src/mqtt_pal.c", 21 | }; 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-mqtt 2 | 3 | A build package for the awesome [mqtt-c](https://github.com/LiamBindle/MQTT-C) project by [Liam Bindle](https://github.com/LiamBindle). 4 | 5 | Right now only provides a build script API in `Sdk.zig`, but might contain a Zig frontend in the future. 6 | 7 | ## Usage 8 | 9 | ```zig 10 | const std = @import("std"); 11 | const Sdk = @import("Sdk.zig"); 12 | 13 | pub fn build(b: *std.build.Builder) void { 14 | const mode = b.standardReleaseOptions(); 15 | const target = b.standardTargetOptions(.{}); 16 | 17 | const lib = Sdk.createLibrary(b); 18 | lib.setBuildMode(mode); 19 | lib.setTarget(target); 20 | lib.install(); 21 | 22 | const exe = b.addExecutable(…); 23 | exe.linkLibary(lib); 24 | exe.addIncludePath(Sdk.include_path); 25 | … 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Felix "xq" Queißner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/MqttClient.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const network = @import("network"); 3 | 4 | const MqttClient = @This(); 5 | 6 | const c = @cImport({ 7 | @cInclude("mqtt.h"); 8 | }); 9 | 10 | socket: ?network.Socket, 11 | client: c.mqtt_client, 12 | 13 | sendbuf: [2048]u8, // sendbuf should be large enough to hold multiple whole mqtt messages 14 | recvbuf: [1024]u8, // recvbuf should be large enough any whole mqtt message expected to be received 15 | 16 | publish_response_callback_ptr: ?*anyopaque, 17 | publish_response_callback: ?*const fn (?*anyopaque, *MqttClient, PublishResponse) void, 18 | 19 | pub fn init(self: *MqttClient) !void { 20 | self.* = .{ 21 | .socket = null, 22 | .client = undefined, 23 | .sendbuf = undefined, 24 | .recvbuf = undefined, 25 | .publish_response_callback_ptr = null, 26 | .publish_response_callback = null, 27 | }; 28 | } 29 | 30 | pub fn setPublishResponseCallback(mqtt: *MqttClient, context: anytype, comptime callback: fn (@TypeOf(context), *MqttClient, PublishResponse) void) void { 31 | const C = @TypeOf(context); 32 | const H = struct { 33 | fn cb(ctx: ?*anyopaque, mq: *MqttClient, resp: PublishResponse) void { 34 | callback(@ptrCast(C, @alignCast(@alignOf(C), ctx)), mq, resp); 35 | } 36 | }; 37 | 38 | mqtt.publish_response_callback = H.cb; 39 | mqtt.publish_response_callback_ptr = context; 40 | mqtt.client.publish_response_callback_state = mqtt; 41 | } 42 | 43 | fn publishCallback(state_ptr: ?*?*anyopaque, publish_ptr: ?*c.mqtt_response_publish) callconv(.C) void { 44 | const mqtt = @ptrCast(*MqttClient, @alignCast(@alignOf(MqttClient), state_ptr.?.*.?)); 45 | 46 | const published = publish_ptr orelse return; 47 | const topic = @ptrCast([*]const u8, published.topic_name.?)[0..published.topic_name_size]; 48 | const message = @ptrCast([*]const u8, published.application_message.?)[0..published.application_message_size]; 49 | 50 | const response = PublishResponse{ 51 | .topic = topic, 52 | .payload = message, 53 | .dup_flag = (published.dup_flag != 0), 54 | .qos_level = @intToEnum(QoS, published.qos_level), 55 | .packet_id = published.packet_id, 56 | .retain_flag = (published.retain_flag != 0), 57 | }; 58 | 59 | if (mqtt.publish_response_callback) |callback| { 60 | callback(mqtt.publish_response_callback_ptr, mqtt, response); 61 | } 62 | } 63 | 64 | pub const PublishResponse = struct { 65 | topic: []const u8, 66 | payload: []const u8, 67 | dup_flag: bool, 68 | qos_level: QoS, 69 | retain_flag: bool, 70 | packet_id: u16, 71 | }; 72 | 73 | pub const LastWill = struct { 74 | topic: [:0]const u8, 75 | message: []const u8, 76 | }; 77 | pub fn connect( 78 | self: *MqttClient, 79 | host_name: []const u8, 80 | port: ?u16, 81 | client_id: ?[:0]const u8, 82 | last_will: ?LastWill, 83 | ) !void { 84 | std.debug.assert(self.socket == null); 85 | 86 | self.socket = try network.connectToHost( 87 | std.heap.page_allocator, 88 | host_name, 89 | port orelse 1883, 90 | .tcp, 91 | ); 92 | 93 | // set socket to nonblocking 94 | const flags = try std.os.fcntl(self.socket.?.internal, std.os.F.GETFL, 0); 95 | _ = try std.os.fcntl(self.socket.?.internal, std.os.F.SETFL, flags | std.os.O.NONBLOCK); 96 | 97 | try wrapMqttErr(c.mqtt_init( 98 | &self.client, 99 | self.socket.?.internal, 100 | &self.sendbuf, 101 | self.sendbuf.len, 102 | &self.recvbuf, 103 | self.recvbuf.len, 104 | publishCallback, 105 | )); 106 | 107 | // Ensure we have a clean session 108 | const connect_flags: u8 = c.MQTT_CONNECT_CLEAN_SESSION; 109 | // Send connection request to the broker. 110 | try wrapMqttErr(c.mqtt_connect( 111 | &self.client, 112 | if (client_id) |ptr| ptr.ptr else null, 113 | if (last_will) |will| will.topic.ptr else null, // last will topic 114 | if (last_will) |will| will.message.ptr else null, // last will message 115 | if (last_will) |will| will.message.len else 0, // last will message len 116 | null, // user name 117 | null, // password 118 | connect_flags, 119 | 400, // keep alive 120 | )); 121 | } 122 | 123 | pub fn deinit(self: *MqttClient) void { 124 | if (self.socket) |*sock| { 125 | sock.close(); 126 | } 127 | self.* = undefined; 128 | } 129 | 130 | pub fn subscribe(self: *MqttClient, topic: [:0]const u8) !void { 131 | try wrapMqttErr(c.mqtt_subscribe( 132 | &self.client, 133 | topic.ptr, 134 | 0, // TODO: Figure out what this exactly does 135 | )); 136 | } 137 | 138 | pub fn sync(self: *MqttClient) !void { 139 | try wrapMqttErr(c.mqtt_sync(&self.client)); 140 | } 141 | 142 | pub fn publish( 143 | self: *MqttClient, 144 | topic: [:0]const u8, 145 | message: []const u8, 146 | qos: QoS, 147 | retain: bool, 148 | ) !void { 149 | const flags: u8 = @intCast(u8, @enumToInt(qos) | if (retain) c.MQTT_PUBLISH_RETAIN else 0); 150 | try wrapMqttErr(c.mqtt_publish( 151 | &self.client, 152 | topic.ptr, 153 | message.ptr, 154 | message.len, 155 | flags, 156 | )); 157 | } 158 | 159 | fn wrapMqttErr(err: c.MQTTErrors) !void { 160 | if (err == c.MQTT_OK) 161 | return; 162 | 163 | std.log.err("mqtt error: {s}\n", .{std.mem.sliceTo(c.mqtt_error_str(err).?, 0)}); 164 | 165 | return switch (err) { 166 | c.MQTT_ERROR_NULLPTR => error.Nullptr, 167 | c.MQTT_ERROR_CONTROL_FORBIDDEN_TYPE => error.ControlForbiddenType, 168 | c.MQTT_ERROR_CONTROL_INVALID_FLAGS => error.ControlInvalidFlags, 169 | c.MQTT_ERROR_CONTROL_WRONG_TYPE => error.ControlWrongType, 170 | c.MQTT_ERROR_CONNECT_CLIENT_ID_REFUSED => error.ConnectClientIdRefused, 171 | c.MQTT_ERROR_CONNECT_NULL_WILL_MESSAGE => error.ConnectNullWillMessage, 172 | c.MQTT_ERROR_CONNECT_FORBIDDEN_WILL_QOS => error.ConnectForbiddenWillQos, 173 | c.MQTT_ERROR_CONNACK_FORBIDDEN_FLAGS => error.ConnackForbiddenFlags, 174 | c.MQTT_ERROR_CONNACK_FORBIDDEN_CODE => error.ConnackForbiddenCode, 175 | c.MQTT_ERROR_PUBLISH_FORBIDDEN_QOS => error.PublishForbiddenQos, 176 | c.MQTT_ERROR_SUBSCRIBE_TOO_MANY_TOPICS => error.SubscribeTooManyTopics, 177 | c.MQTT_ERROR_MALFORMED_RESPONSE => error.MalformedResponse, 178 | c.MQTT_ERROR_UNSUBSCRIBE_TOO_MANY_TOPICS => error.UnsubscribeTooManyTopics, 179 | c.MQTT_ERROR_RESPONSE_INVALID_CONTROL_TYPE => error.ResponseInvalidControlType, 180 | c.MQTT_ERROR_CONNECT_NOT_CALLED => error.ConnectNotCalled, 181 | c.MQTT_ERROR_SEND_BUFFER_IS_FULL => error.SendBufferIsFull, 182 | c.MQTT_ERROR_SOCKET_ERROR => error.SocketError, 183 | c.MQTT_ERROR_MALFORMED_REQUEST => error.MalformedRequest, 184 | c.MQTT_ERROR_RECV_BUFFER_TOO_SMALL => error.RecvBufferTooSmall, 185 | c.MQTT_ERROR_ACK_OF_UNKNOWN => error.AckOfUnknown, 186 | c.MQTT_ERROR_NOT_IMPLEMENTED => error.NotImplemented, 187 | c.MQTT_ERROR_CONNECTION_REFUSED => error.ConnectionRefused, 188 | c.MQTT_ERROR_SUBSCRIBE_FAILED => error.SubscribeFailed, 189 | c.MQTT_ERROR_CONNECTION_CLOSED => error.ConnectionClosed, 190 | c.MQTT_ERROR_INITIAL_RECONNECT => error.InitialReconnect, 191 | c.MQTT_ERROR_INVALID_REMAINING_LENGTH => error.InvalidRemainingLength, 192 | c.MQTT_ERROR_CLEAN_SESSION_IS_REQUIRED => error.CleanSessionIsRequired, 193 | c.MQTT_ERROR_RECONNECTING => error.Reconnecting, 194 | else => error.Unexpected, 195 | }; 196 | } 197 | 198 | pub const QoS = enum(c_int) { 199 | at_most_once = c.MQTT_PUBLISH_QOS_0, 200 | at_least_once = c.MQTT_PUBLISH_QOS_1, 201 | exactly_once = c.MQTT_PUBLISH_QOS_2, 202 | }; 203 | --------------------------------------------------------------------------------