first commit

This commit is contained in:
2023-03-14 11:29:44 +07:00
commit b215f1a3ac
19 changed files with 1061 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.vscode
examples/authfiles

29
Manifest.toml Normal file
View File

@@ -0,0 +1,29 @@
# This file is machine-generated - editing it directly is not advised
[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
[[InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
[[Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
[[Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
[[Random]]
deps = ["Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
[[Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
[[Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

9
Project.toml Normal file
View File

@@ -0,0 +1,9 @@
name = "Mosquitto"
uuid = "db317de6-444b-4dfa-9d0e-fbf3d8dd78ea"
authors = ["Christian Dengler <sumo_spider@yahoo.de>"]
version = "0.4.1"
[deps]
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

164
README.md Normal file
View File

@@ -0,0 +1,164 @@
# Mosquitto.jl
A wrapper around the Mosquitto C Api. The package provides easy to use MQTT client functionality.
## Package Status
* **Linux + Julia v1.6.x** has trouble when using multiple threads. You need to upgrade to 1.7 or use single thread with manual "loop" calls for that specific configuration.
MQTT v5 features like properties are not yet implemented. If you have the need for those, feel free to add an request on Github.
## Installation
* Install the mosquitto library
Follow the instructions at https://mosquitto.org/download/
* Download the julia package
`]add https://github.com/denglerchr/Mosquitto.jl`
## Basic Usage
### Connect to a broker
```julia
using Mosquitto
client = Client("test.mosquitto.org", 1883)
```
Create a client using the ip and port of the broker. If you use >1 julia thread, the network loop will start immediately.
Use ?Mosquitto.Client for information on client settings.
### Publish a message
```julia
topic = "test"
message = "hello world"
publish(client, topic, message)
# only necessary if network loop isnt running in seprate thread
!client.status.loop_status && loop(client)
```
A message can be of type string, or of a type that can be converted to a Vector{UInt8} using reinterpret. If you do not use multiple threads and *loop_start(client)*, publishing might not happen until you call *loop(client)*.
### Subscribe to a topic
```julia
topic = "test"
subscribe(client, topic)
```
The subscription will vanish on disonnect. To automatically reconnect, you should subscribe after a connection was detected. Please look at the example *examples/03_subscribe_conconnect.jl*
### Simple example
This example scripts will
1) create a connection to a public broker
2) subscribes to the topic "jltest"
3) publish 2 messages to the same topic "jltest"
4) read and print the messages.
Note that the script might print 3 messages if a message for that topic is "retained".
```julia
using Mosquitto
# 1)
client = Client("test.mosquitto.org", 1883)
# 2)
topic = "jltest"
subscribe(client, topic)
# 3)
# Send 2 messages, first one will remain in the broker an be received on new connect
publish(client, topic, "Hi from Julia"; retain = true)
publish(client, topic, "Another message"; retain = false)
# lets wait to be sure to receive something
# or call the loop during that time, to make sure stuff is sent/received
client.status.loop_status ? sleep(3) : loop(client; timeout = 500, ntimes = 10)
# 4)
nmessages = Base.n_avail(Mosquitto.messages_channel)
for i = 1:nmessages
msg = take!(Mosquitto.messages_channel) # Tuple{String, Vector{UInt8})
println("Topic: $(msg.topic)\tMessage: $(String(msg.payload))")
end
```
## Advanced Usage and Notes
### Callbacks on messages or connection/disconnection
While the mosquitto C library requires callback functions, this package uses Channels to indicate the receiving of a message or the connection/disconnection to/from a broker. You should `take!(channel)` on these, possibly after checking for the number of available messages if not run in a separate thread. The two channels can be accessed via:
* `get_messages_channel()` or `Mosquitto.messages_channel`
* `get_connect_channel()` or `Mosquitto.connect_channel`
### Use a single threads or multiple threads
For simplicity of use, the network loop is executed in parallel when using multiple threads. This can in some cases lead to problems, e.g., when using multiple clients, as running multiple loops in parallel is not supported currently. Therefore, client loops should be run in sequence, see *examples/multiple_clients.jl* for an example.
### Authentication
You find examples in the example folder for how to use TLS connections and user/password authetication. Currently bad credentials do not lead to any error or warning, your messages will just not be sent and you will not receive any messages.
### Advanced example
```julia
# Read 20 messages in topic "test/..." from the public broker test.mosquitto.org
# Different from the previous example, the client will resubscribe to its topic every time it connects to the broker
using Mosquitto
# Connect to a broker using tls and username/password authetication.
# The CA certificate can be downloaded from the mosquitto page https://test.mosquitto.org/ssl/mosquitto.org.crt
# The connect function will not start a network loop in parallel, loop is triggered manually later.
client = Client()
const cafilepath = ... # add path to ca certificate here
tls_set(client, cafilepath)
connect(client, "test.mosquitto.org", 8885; username = "rw", password = "readwrite")
# Subscribe to topic "test" every time the client connects
# To know if there was a connection/disconnection, the channel Mosquitto.connect_channel
# or get_connect_channel() is used.
function onconnect(c)
# Check if something happened, else return 0
nmessages = Base.n_avail(get_connect_channel())
nmessages == 0 && return 0
# At this point, a connection or disconnection happened
for i = 1:nmessages
conncb = take!(get_connect_channel())
if conncb.val == 1
println("Connection of client $(conncb.clientptr) successfull, subscribing to test/#")
subscribe(c, "test/#")
elseif conncb.val == 0
println("Client $(conncb.clientptr) disconnected")
end
end
return nmessages
end
# Print a message if it is received.
# To know if a message was received, we use the Mosquitto.messages_channel
# or get_messages_channel().
function onmessage(mrcount)
# Check if something happened, else return 0
nmessages = Base.n_avail(get_messages_channel())
nmessages == 0 && return 0
# At this point, a message was received, lets process it
for i = 1:nmessages
temp = take!(get_messages_channel())
println("Message $(mrcount+i):")
message = String(temp.payload)
length(message) > 20 && (message = message[1:18]*"...")
println("\ttopic: $(temp.topic)\tmessage:$(message)")
end
return nmessages
end
# We trigger the loop manually until we have received at least
# 20 messages
mrcount = 0
while mrcount < 20
loop(client) # network loop
onconnect(client) # check for connection/disconnection
mrcount += onmessage(mrcount) # check for messages
end
# Disconnect the client everything
disconnect(client)
```

11
examples/01_publish.jl Normal file
View File

@@ -0,0 +1,11 @@
using Mosquitto, Dates
# connect to a broker, also start loop if Threads.nthreads() > 1
client = Client("test.mosquitto.org")
topic = "test/julia"
message = "Hello World from Julia, send on $(now(UTC)) using the Mosquitto wrapper https://github.com/denglerchr/Mosquitto.jl"
publish(client, topic, message; retain = true)
!client.status.loop_status && loop(client; ntimes = 2)
disconnect(client)

View File

@@ -0,0 +1,32 @@
# Read 20 messages in topic "test/..." from the public broker test.mosquitto.org
using Mosquitto
# Connect to a broker, also starts loop if Threads.nthreads()>1
client = Client("test.mosquitto.org", 1883)
# subscribe to topic "test"
subscribe(client, "test/#")
function onmessage(nmin)
nmessages = Base.n_avail(Mosquitto.messages_channel)
nmessages == 0 && return 0
for i = 1:nmessages
temp = take!(Mosquitto.messages_channel)
println("Message $(nmin + i) of 20:")
println("\ttopic: $(temp.topic)\tmessage:$(String(temp.payload))")
end
return nmessages
end
# Messages will be put in
# the channel Mosquitto.messages_channel.
nmessages = 0
while nmessages<20
# Take the message on arrival
loop(client)
nmessages += onmessage(nmessages)
end
# Close everything
disconnect(client)

View File

@@ -0,0 +1,29 @@
# Read 20 messages in topic "test/..." from the public broker test.mosquitto.org
# This example assumes julia was started with >1 thread
# e.g., julia -t 2 subscribe.jl
if Threads.nthreads()<2
println("Start julia using atleast 2 threads to run this example:")
println("julia -t 2 subscribe.jl")
exit(1)
end
using Mosquitto
# Connect to a broker, also starts loop if Threads.nthreads()>1
client = Client("test.mosquitto.org", 1883)
# subscribe to topic "test"
subscribe(client, "test/#")
# Messages will be put in
# the channel Mosquitto.messages_channel.
for i = 1:20
# Take the message on arrival
temp = take!(Mosquitto.messages_channel)
# Do something with the message
println("Message $i of 20:")
println("\ttopic: $(temp.topic)\tmessage:$(String(temp.payload))")
end
# Close everything
disconnect(client)

View File

@@ -0,0 +1,56 @@
# Read 20 messages in topic "test/..." from the public broker test.mosquitto.org
# Different from example 02, the client will resubscribe to its topic every time it connects to the broker
using Mosquitto
# Connect to a broker, but dont start network loop.
# We will trigger the network loop manually here using the loop function
client = Client("test.mosquitto.org", 1883; startloop = false)
# subscribe to topic "test" every time the client connects
function onconnect(c)
# Check if something happened, else return 0
nmessages = Base.n_avail(get_connect_channel())
nmessages == 0 && return 0
# At this point, a connection or disconnection happened
for i = 1:nmessages
conncb = take!(get_connect_channel())
if conncb.val == 1
println("Connection of client $(conncb.clientptr) successfull, subscribing to test/#")
subscribe(c, "test/#")
elseif conncb.val == 0
println("Client $(conncb.clientptr) disconnected")
end
end
return nmessages
end
function onmessage(mrcount)
# Check if something happened, else return 0
nmessages = Base.n_avail(get_messages_channel())
nmessages == 0 && return 0
# At this point, a message was received, lets process it
for i = 1:nmessages
temp = take!(get_messages_channel())
println("Message $(mrcount+i):")
message = String(temp.payload)
length(message) > 20 && (message = message[1:18]*"...")
println("\ttopic: $(temp.topic)\tmessage:$(message)")
end
return nmessages
end
# Messages will be put as a tuple in
# the channel Mosquitto.messages_channel.
mrcount = 0
while mrcount < 20
loop(client) # network loop
onconnect(client) # check for connection/disconnection
mrcount += onmessage(mrcount) # check for messages
end
# Close everything
disconnect(client)

View File

@@ -0,0 +1,47 @@
# Read 20 messages in topic "test/..." from the public broker test.mosquitto.org
# Different from example 02, the client will resubscribe to its topic every time it connects to the broker
# This example assumes julia was started with >1 thread
# e.g., julia -t 2 subscribe.jl
if Threads.nthreads()<2
println("Start julia using atleast 2 threads to run this example:")
println("julia -t 2 subscribe.jl")
exit(1)
end
using Mosquitto
# Connect to a broker, also starts loop if Threads.nthreads()>1
client = Client("test.mosquitto.org", 1883)
# subscribe to topic "test" every time the client connects
function subonconnect(c)
while true
conncb = take!(get_connect_channel())
if conncb.val == 1
println("Connection of client $(conncb.clientptr) successfull, subscribing to test/#")
subscribe(c, "test/#")
elseif conncb.val == 0
println("Client $(conncb.clientptr) disconnected")
else
println("Subonconnect function returning")
return 0
end
end
end
Threads.@spawn subonconnect(client)
# Messages will be put as a tuple in
# the channel Mosquitto.messages_channel.
for i = 1:50
# Take the message on arrival
temp = take!(get_messages_channel())
# Do something with the message
println("Message $i of 50:")
message = String(temp.payload)
length(message) > 15 && (message = message[1:13]*"...")
println("\ttopic: $(temp.topic)\tmessage:$(message)")
end
# Close everything
put!(Mosquitto.connect_channel, Mosquitto.ConnectionCB(Ptr{Mosquitto.Cmosquitto}(C_NULL), UInt8(255), 0))
disconnect(client)

View File

@@ -0,0 +1,68 @@
# Connect to 3 clients, 1 on the test server of mosquitto and 2 on localhost (requires a broker to run on localhost:1883)
# We run this only with manual loop execution, as multiple client with threaded network loop are currently not supported.
# What this script does:
# The client 1 will subscribe to messages, and every time a message is received in the correct topic test/julia
# client 2 will publish a message to localhost which is again received by client 3
using Mosquitto
# Connect to a broker, also starts loop if Threads.nthreads()>1
client1 = Client("test.mosquitto.org", 1883, startloop = false) # will receive message
client2 = Client("localhost", 1883, startloop = false) # will publish to localhost
client3 = Client("localhost", 1883, startloop = false) # will receive from localhost
# subscribe to topic different topics for each client
function subonconnect(c1::Client, c2::Client, c3::Client)
# check channel, if there is something todo
nmessages = Base.n_avail(get_connect_channel())
nmessages == 0 && return 0
for i = 1:nmessages
conncb = take!(get_connect_channel())
if conncb.val == 1
println("$(conncb.clientptr): connection successfull")
conncb.clientptr == c1.cptr.mosc && subscribe(c1, "test/#")
conncb.clientptr == c3.cptr.mosc && subscribe(c3, "julia")
elseif conncb.val == 0
println("$(conncb.clientptr): disconnected")
end
end
return 0
end
# What to do if there is a message
function onmessage(c1, c2)
rand()<0.1 && publish(c1, "test/julia", "From client 1"; retain = false)
nmessages = Base.n_avail(get_messages_channel())
nmessages == 0 && return 0
for i = 1:nmessages
temp = take!(get_messages_channel())
# Do something with the message
if temp.topic == "test/julia"
println("\ttopic: $(temp.topic)\tmessage:$(String(temp.payload))")
publish(c2, "julia", "From client 2"; qos = 2)
elseif temp.topic == "julia"
println("\ttopic: $(temp.topic)\tmessage:$(String(temp.payload))")
else
println("Wrong topic :(")
end
end
return 0
end
# Messages will be put as a Message struct
# the channel Mosquitto.messages_channel.
for i = 1:200
loop(client1; timeout = 100)
loop(client2; timeout = 100)
loop(client3; timeout = 100)
subonconnect(client1, client2, client3)
onmessage(client1, client2)
end
# Close everything
disconnect(client1)
disconnect(client2)
disconnect(client3)

26
examples/05_tls_cert.jl Normal file
View File

@@ -0,0 +1,26 @@
using Mosquitto
# defined paths to cafile, certfile, keyfile
include("authfiles/certpaths.jl")
# Create client, but dont connect yet
client = Client()
# Configure tls by providing crt and key files, needs to be done before connecting
tls_set(client, cafile; certfile = certfile, keyfile = keyfile)
# Connect
connect(client, "test.mosquitto.org", 8884)
# Rest as usual, subscribe and publish and read messages
subscribe(client, "test")
publish(client, "test/julia", "hello"; retain = false)
client.status.loop_status ? sleep(1) : loop(client; ntimes = 10)
nmessages = Base.n_avail(Mosquitto.messages_channel)
for i = 1:nmessages
msg = take!(Mosquitto.messages_channel) # Tuple{String, Vector{UInt8})
println("Topic: $(msg.topic)\tMessage: $(String(msg.payload))")
end
disconnect(client)

26
examples/06_tls_pw.jl Normal file
View File

@@ -0,0 +1,26 @@
using Mosquitto
# import paths to the server ca file (can be downloaded from https://test.mosquitto.org/ )
include("authfiles/certpaths.jl")
# Create client, but dont connect yet
client = Client()
# Configure tls using the ca certificate, needs to be done before connecting
tls_set(client, cafile)
# Connect using username and password
connect(client, "test.mosquitto.org", 8885; username = "rw", password = "readwrite")
# Rest as usual, subscribe and publish and read messages
subscribe(client, "test")
publish(client, "test/julia", "hello"; retain = false)
client.status.loop_status ? sleep(1) : loop(client; ntimes = 10)
nmessages = Base.n_avail(Mosquitto.messages_channel)
for i = 1:nmessages
msg = take!(Mosquitto.messages_channel) # Tuple{String, Vector{UInt8})
println("Topic: $(msg.topic)\tMessage: $(String(msg.payload))")
end
disconnect(client)

39
src/Mosquitto.jl Normal file
View File

@@ -0,0 +1,39 @@
# Documentation
# https://mosquitto.org/api/files/mosquitto-h.html#mosquitto_message_callback_set
# https://github.com/eclipse/mosquitto/blob/master/include/mosquitto.h
module Mosquitto
import Base.finalizer
using Random, Libdl
# find library
const libmosquitto = @static if Sys.isunix()
Libdl.find_library(["libmosquitto", "libmosquitto.so.1"])
elseif Sys.iswindows()
Libdl.find_library("mosquitto.dll", [raw"C:\Program Files\Mosquitto"])
end
function __init__()
libmosquitto == "" && throw("Could not find the mosquitto library. If you're sure that it's installed, try adding it to DL_LOAD_PATH and rebuild the package.")
mosq_error_code = ccall((:mosquitto_lib_init, libmosquitto), Cint, ())
mosq_error_code != 0 && println("Mosquitto init returned error code $mosq_error_code")
v = lib_version()
v[1] != 2 || v[2] != 0 && println("Found lib version $(v[1]).$(v[2]), which is different from 2.0. Some functionality might not work")
atexit(lib_cleanup)
end
include("helpers.jl")
include("cwrapper.jl")
export lib_version
include("callbacks.jl")
export get_messages_channel, get_connect_channel
include("client.jl")
export Client, connect, reconnect, disconnect, publish, subscribe, unsubscribe, loop, tls_set, tls_psk_set
include("looprunner.jl")
export loop_start, loop_stop
end # module

84
src/callbacks.jl Normal file
View File

@@ -0,0 +1,84 @@
"""
struct MessageCB with fields
* topic:: String
* payload::Vector{UInt8}
A struct containing incoming message information and payload.
"""
struct MessageCB
topic::String
payload::Vector{UInt8}
end
"""
struct ConnectionCB with fields
* clientptr::Ptr
* val::UInt8
* returncode::Cint
The clientptr contains the ptr of the client that connected or disconnected.
This allows to distinguish between clients.
val is 0 on disconnect and 1 on connect.
returncode is the MQTT return code which can be used to identify, e.g., the reason for a disconnect.
"""
struct ConnectionCB
clientptr::Ptr{Cmosquitto}
val::UInt8
returncode::Cint
end
const messages_channel = Channel{MessageCB}(20)
const connect_channel = Channel{ConnectionCB}(5)
"""
get_messages_channel()
Returns the channel to which received messages are sent. The channel is a Channel{MessageCB}(20).
See ?Mosquitto.MessageCB for information on the struct
"""
get_messages_channel() = messages_channel
"""
get_connect_channel()
Returns the channel to which event notifications for connections or disconnections are sent. The channel is a Channel{ConnectionCB}(5).
See ?Mosquitto.ConnectionCB for information on the struct
"""
get_connect_channel() = connect_channel
# This callback function puts any message on arrival in the channel
# messages_channel which is a Channel{Mosquitto.Message}(20)
function callback_message(mos::Ptr{Cmosquitto}, obj::Ptr{Cvoid}, message::Ptr{CMosquittoMessage}) #, clientid::String)
# get topic and payload from the message
jlmessage = unsafe_load(message)
jlpayload = [unsafe_load(jlmessage.payload, i) for i = 1:jlmessage.payloadlen]
topic = unsafe_string(jlmessage.topic)
# put it in the channel for further use
if Base.n_avail(messages_channel)>=messages_channel.sz_max
take!(messages_channel)
end
put!(messages_channel, MessageCB(topic, jlpayload))
return nothing
end
function callback_connect(mos::Ptr{Cmosquitto}, obj::Ptr{Cvoid}, rc::Cint)
if Base.n_avail(connect_channel)>=connect_channel.sz_max
take!(connect_channel)
end
put!( connect_channel, ConnectionCB(mos, one(UInt8), rc ) )
return nothing
end
function callback_disconnect(mos::Ptr{Cmosquitto}, obj::Ptr{Cvoid}, rc::Cint)
if Base.n_avail(connect_channel)>=connect_channel.sz_max
take!(connect_channel)
end
put!( connect_channel, ConnectionCB(mos, zero(UInt8), rc ) )
return nothing
end

173
src/client.jl Normal file
View File

@@ -0,0 +1,173 @@
import Base.n_avail, Base.show
struct Cobjs
mosc::Ref{Cmosquitto}
obj::Ref{Cvoid}
conncb::Ref{Cvoid}
dconncb::Ref{Cvoid}
end
mutable struct MoscStatus
conn_status::Bool
loop_status::Bool
end
struct Client
id::String
cptr::Cobjs
loop_channel::AbstractChannel{Int}
status::MoscStatus
end
function show(io::IO, client::Client)
println("MQTTClient_$(client.id)")
end
function finalizer(client::Client)
disconnect(client)
destroy(client.cptr.mosc)
end
"""
Client(ip::String, port::Int=1883; kwargs...)
Create a client connection to an MQTT broker. Possible key word arguments are:
* id::String = randstring(15) The id should be unique per connection.
* connectme::Bool = true Connect immediately if true. If false, you need to manually use *connect(client, ip, port)* and input arguments are not used.
* startloop::Bool = true If true, and Threads.nthreads()>1, the network loop will be executed regularly after connection.
Client( ; id::String = randstring(15))
Create a client structure without connecting to a broker or starting a network loop.
"""
function Client(ip::String, port::Int=1883; id::String = randstring(15), connectme::Bool = true, startloop::Bool = true)
# Create a Client object
client = Client( ; id = id )
# Possibly Connect to broker
if connectme
flag = connect(client, ip, port)
flag != 0 && println("Connection to the broker failed")
# Start loop if it can be started without blocking
if flag == 0 && startloop && Threads.nthreads()>1
loop_start(client)
elseif startloop
println("Single thread, loop will be blocking, start it manually using loop_start(::Client) or call loop(client) regularly.")
end
end
return client
end
function Client(; id::String = randstring(15))
# Create mosquitto object
cobj = Ref{Cvoid}()
cmosc = mosquitto_new(id, true, cobj)
# Set callbacks
#f_message_cb(mos, obj, message) = callback_message(mos, obj, message, id)
cfunc_message = @cfunction(callback_message, Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}, Ptr{CMosquittoMessage}))
message_callback_set(cmosc, cfunc_message)
cfunc_connect = @cfunction(callback_connect, Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}, Cint))
connect_callback_set(cmosc, cfunc_connect)
cfunc_disconnect = @cfunction(callback_disconnect, Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}, Cint))
disconnect_callback_set(cmosc, cfunc_disconnect)
# Create object
loop_channel = Channel{Int}(1)
return Client(id, Cobjs(cmosc, cobj, cfunc_connect, cfunc_disconnect), loop_channel, MoscStatus(false, false) )
end
"""
connect(client::Client, ip::String, port::Int; kwargs...)
Connect the client to a broker. kwargs are:
* username::String = "" A username, should one be required
* password::String = "" A password belonging to the username
* keepalive::Int = 60 Maximal of time the client has to send PINGREQ or a message before disconnection
"""
function connect(client::Client, ip::String, port::Int; username::String = "", password::String = "", keepalive::Int = 60)
if username != ""
flag = username_pw_set(client.cptr.mosc, username, password)
flag != 0 && println("Couldnt set password and username, error $flag")
end
flag = connect(client.cptr.mosc, ip; port = port, keepalive = keepalive)
flag == 0 ? (client.status.conn_status = true) : println("Connection to broker failed")
return flag
end
"""
disconnect(client::Client)
"""
function disconnect(client::Client)
client.status.loop_status && loop_stop(client)
flag = disconnect(client.cptr.mosc)
flag == 0 && (client.status.conn_status = false)
return flag
end
"""
reconnect(client::Client)
"""
function reconnect(client::Client)
flag = reconnect(client.cptr.mosc)
flag == 0 && (client.status.conn_status = true)
return flag
end
"""
publish(client::Client, topic::String, payload; qos::Int = 1, retain::Bool = true)
Publish a message to the broker.
"""
publish(client::Client, topic::String, payload; qos::Int = 1, retain::Bool = true) = publish(client.cptr.mosc, topic, payload; qos = qos, retain = retain)
"""
subscribe(client::Client, topic::String; qos::Int = 1)
Subscribe to a topic. Received messages will be accessible Mosquitto.messages_channel as a Tuple{String, Vector{Uint8}}.
"""
subscribe(client::Client, topic::String; qos::Int = 1) = subscribe(client.cptr.mosc, topic; qos = qos)
"""
unsubscribe(client::Client, topic::String)
Unsubscribe from a topic.
"""
unsubscribe(client::Client, topic::String) = unsubscribe(client.cptr.mosc, topic)
"""
tls_set(client::Client, cafile::String; certfile::String = "", keyfile::String = "")
"""
function tls_set(client::Client, cafile::String; certfile::String = "", keyfile::String = "")
xor( certfile == "", keyfile == "" ) && throw("You need to either provide both cert and key files, or none of both")
if certfile == ""
return tls_set(client.cptr.mosc, cafile, C_NULL, C_NULL, C_NULL, C_NULL)
else
return tls_set(client.cptr.mosc, cafile, C_NULL, certfile, keyfile, C_NULL)
end
end
"""
tls_plk_set(client::Client, psk::String, identity::String, ciphers::Union{Nothing, String})
"""
function tls_psk_set(client::Client, psk::String, identity::String, ciphers::Union{Nothing, String} = nothing)
return tls_psk_set(client.cptr.mosc, psk, identity, ciphers)
end

137
src/cwrapper.jl Normal file
View File

@@ -0,0 +1,137 @@
struct Cmosquitto end
struct CMosquittoMessage
mid::Cint
topic::Cstring
payload::Ptr{UInt8} # we treat payload as raw bytes
payloadlen::Cint
qos::Cint
retain::Bool
end
function mosquitto_new(id::String, clean_start::Bool, obj)
return ccall((:mosquitto_new, libmosquitto), Ptr{Cmosquitto}, (Cstring, Bool, Ptr{Cvoid}), id, clean_start, obj)
end
function destroy(client::Ref{Cmosquitto})
return ccall((:mosquitto_destroy, libmosquitto), Cvoid, (Ptr{Cmosquitto},), client)
end
finalizer(client::Ref{Cmosquitto}) = destroy(client)
function connect(client::Ref{Cmosquitto}, host::String; port::Int = 1883, keepalive::Int = 60)
return ccall((:mosquitto_connect, libmosquitto), Cint, (Ptr{Cmosquitto}, Cstring, Cint, Cint), client, host, port, keepalive)
end
function reconnect(client::Ref{Cmosquitto})
ccall((:mosquitto_reconnect, libmosquitto), Cint, (Ptr{Cmosquitto},), client)
end
function disconnect(client::Ref{Cmosquitto})
return ccall((:mosquitto_disconnect, libmosquitto), Cint, (Ptr{Cmosquitto},), client)
end
function publish(client::Ref{Cmosquitto}, topic::String, payload; qos::Int = 1, retain::Bool = true)
payloadnew = getbytes(payload)
payloadlen = length(payloadnew) # dont use sizeof, as payloadnew might be of type "reinterpreted"
mid = Int[0]
msg_nr = ccall((:mosquitto_publish, libmosquitto), Cint,
(Ptr{Cmosquitto}, Ptr{Cint}, Cstring, Cint, Ptr{UInt8}, Cint, Bool),
client, mid, topic, payloadlen, payloadnew, qos, retain)
return msg_nr
end
function subscribe(client::Ref{Cmosquitto}, sub::String; qos::Int = 1)
mid = zeros(Cint, 1)
msg_nr = ccall((:mosquitto_subscribe, libmosquitto), Cint,
(Ptr{Cmosquitto}, Ptr{Cint}, Cstring, Cint),
client, mid, sub, qos)
return msg_nr
end
function unsubscribe(client::Ref{Cmosquitto}, sub::String)
mid = zeros(Cint, 1)
msg_nr = ccall((:mosquitto_unsubscribe, libmosquitto), Cint,
(Ptr{Cmosquitto}, Ptr{Cint}, Cstring),
client, mid, sub)
return msg_nr
end
#= Broken?
function loop_start(client::Ref{Cmosquitto})
msg_nr = ccall((:mosquitto_loop_start, libmosquitto), Cint, (Ptr{Cmosquitto},), client)
return msg_nr
end
function loop_stop(client::Ref{Cmosquitto}; force::Bool = false)
msg_nr = ccall((:mosquitto_loop_stop, libmosquitto), Cint, (Ptr{Cmosquitto}, Bool), client, force)
return msg_nr
end
=#
function loop_forever(client; timeout::Int = 1000, max_packets::Int = 1)
return ccall((:mosquitto_loop_forever, libmosquitto), Cint, (Ptr{Cmosquitto}, Cint, Cint), client, timeout, max_packets)
end
function loop(client; timeout::Int = 1000, max_packets::Int = 1)
return ccall((:mosquitto_loop, libmosquitto), Cint, (Ptr{Cmosquitto}, Cint, Cint), client, timeout, max_packets)
end
function connect_callback_set(client::Ref{Cmosquitto}, cfunc)
return ccall((:mosquitto_connect_callback_set, libmosquitto), Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}), client, cfunc)
end
function disconnect_callback_set(client::Ref{Cmosquitto}, cfunc)
return ccall((:mosquitto_disconnect_callback_set, libmosquitto), Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}), client, cfunc)
end
function message_callback_set(client::Ref{Cmosquitto}, cfunc)
ccall((:mosquitto_message_callback_set, libmosquitto), Cvoid, (Ptr{Cmosquitto}, Ptr{Cvoid}), client, cfunc)
return nothing
end
function username_pw_set(client::Ref{Cmosquitto}, username::String, password::String)
#password != "" && (password = Cstring(C_NULL))
return ccall((:mosquitto_username_pw_set, libmosquitto), Cint, (Ptr{Cmosquitto}, Cstring, Cstring), client, username, password)
end
function tls_set(client::Ref{Cmosquitto}, cafile, capath, certfile, keyfile, callback::Ptr{Cvoid})
return ccall((:mosquitto_tls_set, libmosquitto), Cint, (Ptr{Cmosquitto}, Cstring, Cstring, Cstring, Cstring, Ptr{Cvoid}), client, cafile, capath, certfile, keyfile, callback)
end
function tls_psk_set(client::Ref{Cmosquitto}, psk::String, identity::String, ciphers::Nothing)
return ccall((:mosquitto_tls_psk_set, libmosquitto), Cint, (Ptr{Cmosquitto}, Cstring, Cstring, Cstring), client, psk, identity, C_NULL)
end
function tls_psk_set(client::Ref{Cmosquitto}, psk::String, identity::String, ciphers::String)
return ccall((:mosquitto_tls_psk_set, libmosquitto), Cint, (Ptr{Cmosquitto}, Cstring, Cstring, Cstring), client, psk, identity, ciphers)
end
function lib_version()
maj = zeros(Int, 1)
min = zeros(Int, 1)
rev = zeros(Int, 1)
ccall((:mosquitto_lib_version, libmosquitto), Cint, (Ptr{Cint}, Ptr{Cint}, Ptr{Cint}), maj, min, rev)
return maj[1], min[1], rev[1]
end
function lib_cleanup()
ccall((:mosquitto_lib_cleanup, libmosquitto), Cvoid, ())
end

5
src/helpers.jl Normal file
View File

@@ -0,0 +1,5 @@
# The function getbytes transforms the payload into a Vector representation of UInt8
@inline getbytes(in::String) = transcode(UInt8, in)
@inline getbytes(in::AbstractVector{UInt8}) = in
@inline getbytes(in::Number) = reinterpret(UInt8, [in])
@inline getbytes(in) = reinterpret(UInt8, in)

75
src/looprunner.jl Normal file
View File

@@ -0,0 +1,75 @@
"""
loop(client::Client; timeout::Int = 1000, ntimes::Int = 1)
Perform a network loop. This will get messages of subscriptions and send published messages.
"""
function loop(client::Client; timeout::Int = 1000, ntimes::Int = 1, autoreconnect::Bool = true)
out = zero(Cint)
for _ = 1:ntimes
out = loop(client.cptr.mosc; timeout = timeout)
if autoreconnect && out == 4
flag = reconnect(client)
client.status.conn_status = ifelse( flag == 0, true, false )
end
end
return out
end
"""
loop_start(client::Client; autoreconnect::Bool = true)
This function keeps calling the network loop until loop_stop is called.
If only one thread is used, this function will be blocking, else the calls
will be executed on a worker thread.
"""
function loop_start(client::Client; autoreconnect::Bool = true)
if client.status.loop_status == true
println("Loop is already running")
return 1
end
if Threads.nthreads()>1
client.status.loop_status = true
Threads.@spawn loop_runner(client, autoreconnect)
else
client.status.loop_status = true
loop_forever(client.cptr.mosc)
end
return 0
end
"""
loop_stop(client::Client)
Stop the network loop.
"""
function loop_stop(client::Client)
if client.status.loop_status
put!(client.loop_channel, 0)
return fetch(client.loop_channel)
else
println("Loop not running")
return 0
end
end
function loop_runner(client::Client, autoreconnect::Bool)
while isempty(client.loop_channel)
msg = loop(client.cptr.mosc)
if autoreconnect && msg == Cint(4)
# case of a disconnect, try reconnecting every 2 seconds
println("Client disconnected, trying to reconnect...")
reconnect(client) != 0 && sleep(2)
elseif msg != Cint(0)
client.status.loop_status = false
println("Loop failed with error $msg")
return msg
end
end
client.status.loop_status = false
return take!(client.loop_channel)
end

49
test/runtests.jl Normal file
View File

@@ -0,0 +1,49 @@
using Mosquitto, Test, Random
topic = "jltest"*randstring(5)
message = [1, 2, 3]
client = Client("test.mosquitto.org", 1883)
@testset "General" begin
Threads.nthreads()>1 && @test client.status.loop_status == 1
Threads.nthreads()==1 && @test client.status.loop_status == 0
@test loop_stop(client) == 0
end
@testset "Unauthenticated" begin
@test subscribe(client, topic) == 0
@test loop(client) == 0
while !isempty(Mosquitto.messages_channel)
# empty channel
take!(Mosquitto.messages_channel)
end
@test publish(client, topic, message; retain = false) == 0
loop(client, ntimes = 10)
@test Base.n_avail(Mosquitto.messages_channel) == 1
if Base.n_avail(Mosquitto.messages_channel) >= 1
@test Array(reinterpret(Int, take!(Mosquitto.messages_channel).payload)) == message
end
@test disconnect(client) == 0
end
client = Client("", 0; connectme = false)
@testset "Authenticated" begin
@test connect(client, "test.mosquitto.org", 1884; username = "rw", password = "readwrite") == 0
@test subscribe(client, topic) == 0
@test loop(client) == 0
while !isempty(Mosquitto.messages_channel)
# empty channel
take!(Mosquitto.messages_channel)
end
@test publish(client, topic, message; retain = false) == 0
loop(client, ntimes = 5)
loop(client; ntimes = 2, timeout = 5000)
@test Base.n_avail(Mosquitto.messages_channel) == 1
if Base.n_avail(Mosquitto.messages_channel) >= 1
@test Array(reinterpret(Int, take!(Mosquitto.messages_channel).payload)) == message
end
@test disconnect(client) == 0
end