Skip to content

Commit

Permalink
basic support for sending websocket messages on state changes
Browse files Browse the repository at this point in the history
  • Loading branch information
tjeb committed Aug 7, 2018
1 parent ee3bbf0 commit 39dc335
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 1 deletion.
98 changes: 97 additions & 1 deletion web_ui/spin_webui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ local sys_stat = require "posix.sys.stat"
local mqtt = require 'mosquitto'
local json = require 'json'

local ws_ext = require 'ws_ext'

local TRAFFIC_CHANNEL = "SPIN/traffic"
local HISTORY_SIZE = 600

Expand Down Expand Up @@ -229,6 +231,7 @@ function handler:add_device_seen(mac, name, timestamp)
self.devices_seen[mac]['lastSeen'] = timestamp
self.devices_seen[mac]['name'] = name
self.devices_seen[mac]['appliedProfiles'] = self.profile_manager:get_device_profiles(mac)
self:send_websocket_update("deviceUpdate", self.devices_seen[mac])
else
local device_data = {}
device_data['lastSeen'] = timestamp
Expand All @@ -244,6 +247,7 @@ function handler:add_device_seen(mac, name, timestamp)
-- this device is new, so send a notification
local notification_txt = "New device on network! Please set a profile"
self:create_notification("new_device", {}, notification_txt, mac, name)
self:send_websocket_update("newDevice", self.devices_seen[mac])
end
self.devices_seen_updated = get_time_string()
end
Expand Down Expand Up @@ -506,6 +510,20 @@ function handler:handle_device_list(request, response)
return response
end

function handler:send_websocket_update(name, arguments)
print("[XX] WEBSOCKCLIENTCOUNT: " .. table.getn(self.websocket_clients))
local msg = ""
if args == nil then
c:send('{"type": "update", "name": "' + name + '"}')
else
c:send('{"type": "update", "name": "' + name + '", "args": ' + json.encode(args) + '}')
end

for i,c in pairs(self.websocket_clients) do
c:send(msg)
end
end

function handler:handle_profile_list(request, response)
self:set_api_headers(response)
local profile_list = {}
Expand Down Expand Up @@ -549,6 +567,7 @@ function handler:handle_device_profiles(request, response, device_mac)
if status then
local notification_txt = "Profile set to " .. profile_name
self:create_notification("profile_set_to", { profile_name }, notification_txt, device_mac, device_name)
self:send_websocket_update("deviceProfileUpdate", { deviceName=device_name, profileName=profile_name })
else
local notification_txt = "Error setting device profile: " .. err
self:create_notification("profile_set_error", { err }, notification_txt, device_mac, device_name)
Expand Down Expand Up @@ -669,6 +688,79 @@ function handler:handle_notification_add(request, response)
return response
end


local data_printer = function(ws)
print("data_printer")
local message = "{\"foo\": \"bar\"}"
--copas.send(ws, message)
-- any messages we want to have sent by default could go here.
-- in principle we only send stuff from other methods
--for i=1,3 do
-- print("send "..i)
-- ws:send(message)
-- copas.sleep(1)
--end
--ws:close()
return "foo"
end

local ws_opts =
{
-- listen on port 8080
--port = 8080,
-- the protocols field holds
-- key: protocol name
-- value: callback on new connection
protocols = {
-- this callback is called, whenever a new client connects.
-- ws is a new websocket instance
echo = echo_handler
},
default = data_printer
}


function handler:handle_websocket(request, response)
-- try to upgrade to a websocket connection.
-- if successful, we add it to the list of websockets (there may be more...)
print(request.raw_sock)
local flat_headers = {}
table.insert(flat_headers, request.http_line)
for h,v in pairs(request.headers) do
table.insert(flat_headers, h:lower() .. ": " .. v)
end
table.insert(flat_headers, "\r\n")
--print("[XX] FLAT HEADERS:")
--for fh,fv in pairs(flat_headers) do
-- print("[XX] " .. fv)
--end
--print("[XX] END OF FLAT HEADERS OF TYPE " .. type(flat_headers))
request.raw_sock:settimeout(1)
print("[XX] AAAAAA")
status, err = self.ws_handler.add_client(flat_headers, request.raw_sock, self)
print("[XX] BBBBBB")
if not status then
print("[XX] CCCCCC")
response:set_status(400, "Bad request")
response.content = err
return response
else
print("[XX] DDDDDD")
table.insert(self.websocket_clients, status)
print("[XX] NEW CONNECT NOW COUNT: " .. table.getn(self.websocket_clients))
end
print("[XX] yoyo yoyo")

-- websocket took over the connection, return nil so minittp
-- does not send a response
return nil
end

function handler:do_add_ws_c(client)
print("[XX] FOO ADDING CLINET")
table.insert(self.websocket_clients, client)
end

-- TODO_MOVE_ENDS_HERE

function handler:init(args)
Expand All @@ -686,6 +778,9 @@ function handler:init(args)
self.notifications_updated = get_time_string()
self.notification_counter = 1

self.websocket_clients = {}
self.ws_handler = ws_ext.ws_server_create(ws_opts)

-- We will use this list for the fixed url mappings
-- Fixed handlers are interpreted as they are; they are
-- ONLY valid for the EXACT path identified in this list
Expand All @@ -701,7 +796,8 @@ function handler:init(args)
["/spin_api/profiles"] = self.handle_profile_list,
["/spin_api/notifications"] = self.handle_notification_list,
["/spin_api/notifications/create"] = self.handle_notification_add,
["/spin_api/configuration"] = self.handle_configuration
["/spin_api/configuration"] = self.handle_configuration,
["/spin_api/ws"] = self.handle_websocket
}

-- Pattern handlers are more flexible than fixed handlers;
Expand Down
153 changes: 153 additions & 0 deletions web_ui/ws_ext.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
local socket = require'socket'
local copas = require'copas'
local tools = require'websocket.tools'
local frame = require'websocket.frame'
local handshake = require'websocket.handshake'
local sync = require'websocket.sync'
local tconcat = table.concat
local tinsert = table.insert

local clients = {}

local client = function(sock,protocol)
local copas = require'copas'

local self = {}

self.state = 'OPEN'
self.is_server = true

self.sock_send = function(self,...)
return copas.send(sock,...)
end

self.sock_receive = function(self,...)
return copas.receive(sock,...)
end

self.sock_close = function(self)
sock:shutdown()
sock:close()
end

self = sync.extend(self)

self.on_close = function(self)
clients[protocol][self] = nil
end

self.broadcast = function(self,...)
for client in pairs(clients[protocol]) do
if client ~= self then
client:send(...)
end
end
self:send(...)
end

return self
end

local ws_server_create = function (opts)
assert(opts and (opts.protocols or opts.default))
local on_error = opts.on_error or function(s) print(s) end
if err then
print("[XX] error err")
error(err)
end
local protocols = {}
if opts.protocols then
for protocol in pairs(opts.protocols) do
clients[protocol] = {}
tinsert(protocols,protocol)
end
end
-- true is the 'magic' index for the default handler
clients[true] = {}

local self = {}
self.add_client = function(request, sock, main_handler)
--local request = {}
--repeat
-- -- no timeout used, so should either return with line or err
-- local line,err = sock:receive('*l')
-- if line then
-- request[#request+1] = line
-- else
-- sock:close()
-- if on_error then
-- on_error('invalid request')
-- end
-- return
-- end
--until line == ''
local upgrade_request = tconcat(request,'\r\n')
print("[XX] REQUEST:")
print(upgrade_request)
print("[XX] END OF REQUEST")
--local status,response,protocol = handshake.accept_upgrade(upgrade_request,protocols)
local status,response,protocol = pcall(handshake.accept_upgrade, upgrade_request,protocols)
if not status then
print("[XX] error: Client request does not appear to be a websocket\n")
--copas.send(sock,protocol)
--sock:close()
return nil, "Client request does not appear to be a websocket"
end
copas.send(sock,response)
local handler
local new_client
local protocol_index
if protocol and opts.protocols[protocol] then
protocol_index = protocol
handler = opts.protocols[protocol]
elseif opts.default then
-- true is the 'magic' index for the default handler
protocol_index = true
handler = opts.default
else
sock:close()
if on_error then
on_error('bad protocol, and no default set')
end
return nil, 'Unknown protocol and no default protocol set'
end
new_client = client(sock,protocol_index)
clients[protocol_index][new_client] = true
--handler(new_client)
print("[XX] ADD TO SERVER")
main_handler:do_add_ws_c(new_client)
print("[XX] ADDED TO SERVER")
-- this is a dirty trick for preventing
-- copas from automatically and prematurely closing
-- the socket
while new_client.state ~= 'CLOSED' do
local dummy = {
send = function() end,
close = function() end
}
copas.send(dummy)
end
return true
end

self.close = function(_,keep_clients)
--copas.removeserver(listener)
listener = nil
if not keep_clients then
for protocol,clients in pairs(clients) do
for client in pairs(clients) do
client:close()
end
end
end
end
return self
end

local _M = {}
_M.ws_server_create = ws_server_create
return _M

--return {
-- ws_server_create = ws_server_create
--}

0 comments on commit 39dc335

Please sign in to comment.