· 7 years ago · Nov 13, 2018, 06:00 PM
1
2--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
3-- Driver Declarations
4--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
5--[[
6 Command Handler Tables
7--]]
8EX_CMD = {}
9PRX_CMD = {}
10NOTIFY = {}
11DEV_MSG = {}
12LUA_ACTION = {}
13
14--[[
15Tables of functions
16The following tables are function containers that are called within the following functions:
17
18 OnDriverInit()
19 - first calls all functions contained within ON_DRIVER_EARLY_INIT table
20 - then calls all functions contained within ON_DRIVER_INIT table
21 OnDriverLateInit()
22 - calls all functions contained within ON_DRIVER_LATEINIT table
23 OnDriverUpdate()
24 - calls all functions contained within ON_DRIVER_UPDATE table
25 OnDriverDestroyed()
26 - calls all functions contained within ON_DRIVER_DESTROYED table
27 OnPropertyChanged()
28 - calls all functions contained within ON_PROPERTY_CHANGED table
29--]]
30ON_DRIVER_INIT = {}
31ON_DRIVER_EARLY_INIT = {}
32ON_DRIVER_LATEINIT = {}
33ON_DRIVER_UPDATE = {}
34ON_DRIVER_DESTROYED = {}
35ON_PROPERTY_CHANGED = {}
36
37-- Constants
38DEFAULT_PROXY_BINDINGID = 5001
39
40--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
41-- Common Driver Code
42--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
43--[[
44 OnPropertyChanged
45 Function called by Director when a property changes value.
46 Parameters
47 sProperty
48 Name of property that has changed.
49 Remarks
50 The value of the property that has changed can be found with: Properties[sName]. Note
51 that OnPropertyChanged is not called when the Property has been changed by the driver
52 calling the UpdateProperty command, only when the Property is changed by the user from
53 the Properties Page. This function is called by Director when a property changes value.
54--]]
55
56_G.id = ""
57
58local random = math.random
59local function uuid()
60 local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
61 return string.gsub(template, '[xy]', function (c)
62 local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
63 return string.format('%x', v)
64 end)
65end
66
67
68function OnPropertyChanged(sProperty)
69 id = uuid()
70
71 Dbg:Trace("OnPropertyChanged(" .. sProperty .. ") changed to: " .. Properties[sProperty])
72
73 local propertyValue = Properties[sProperty]
74
75 -- Remove any spaces (trim the property)
76 local trimmedProperty = string.gsub(sProperty, " ", "")
77
78 -- if function exists then execute (non-stripped)
79 if (ON_PROPERTY_CHANGED[sProperty] ~= nil and type(ON_PROPERTY_CHANGED[sProperty]) == "function") then
80 ON_PROPERTY_CHANGED[sProperty](propertyValue)
81 return
82 -- elseif trimmed function exists then execute
83 elseif (ON_PROPERTY_CHANGED[trimmedProperty] ~= nil and type(ON_PROPERTY_CHANGED[trimmedProperty]) == "function") then
84 ON_PROPERTY_CHANGED[trimmedProperty](propertyValue)
85 return
86 end
87end
88
89function ON_PROPERTY_CHANGED.DebugMode(propertyValue)
90 gDebugTimer:KillTimer()
91 Dbg:OutputPrint(propertyValue:find("Print") ~= nil)
92 Dbg:OutputC4Log(propertyValue:find("Log") ~= nil)
93 if (propertyValue == "Off") then return end
94 gDebugTimer:StartTimer()
95end
96
97function ON_PROPERTY_CHANGED.DebugLevel(propertyValue)
98 Dbg:SetLogLevel(tonumber(string.sub(propertyValue, 1, 1)))
99end
100
101---------------------------------------------------------------------
102-- ExecuteCommand Code
103---------------------------------------------------------------------
104--[[
105 ExecuteCommand
106 Function called by Director when a command is received for this DriverWorks driver.
107 This includes commands created in Composer programming.
108 Parameters
109 sCommand
110 Command to be sent
111 tParams
112 Lua table of parameters for the sent command
113--]]
114function ExecuteCommand(sCommand, tParams)
115 Dbg:Trace("ExecuteCommand(" .. sCommand .. ")")
116 Dbg:Info(tParams)
117
118 -- Remove any spaces (trim the command)
119 local trimmedCommand = string.gsub(sCommand, " ", "")
120
121 -- if function exists then execute (non-stripped)
122 if (EX_CMD[sCommand] ~= nil and type(EX_CMD[sCommand]) == "function") then
123 EX_CMD[sCommand](tParams)
124 -- elseif trimmed function exists then execute
125 elseif (EX_CMD[trimmedCommand] ~= nil and type(EX_CMD[trimmedCommand]) == "function") then
126 EX_CMD[trimmedCommand](tParams)
127 -- handle the command
128 elseif (EX_CMD[sCommand] ~= nil) then
129 QueueCommand(EX_CMD[sCommand])
130 else
131 Dbg:Alert("ExecuteCommand: Unhandled command = " .. sCommand)
132 end
133end
134
135--[[
136 Define any functions of commands (EX_CMD.<command>) received from ExecuteCommand that need to be handled by the driver.
137--]]
138
139function EX_CMD.SendSMS()
140 SendSMS()
141end
142
143function EX_CMD.SendCustomSMS(tParams)
144 Dbg:Debug("SendCustomSMS Phone Number = " .. tParams["Phone Number"])
145 Dbg:Debug("SendCustomSMS Message = " .. tParams["Message"])
146
147--[[ Send the SMS and pass the phone number and message ]]--
148 SendSMS(tParams["Phone Number"], tParams["Message"])
149end
150
151--[[
152 EX_CMD.LUA_ACTION
153 Function called for any actions executed by the user from the Actions Tab in Composer.
154--]]
155function EX_CMD.LUA_ACTION(tParams)
156 if tParams ~= nil then
157 for cmd,cmdv in pairs(tParams) do
158 if cmd == "ACTION" then
159 if (LUA_ACTION[cmdv] ~= nil) then
160 LUA_ACTION[cmdv]()
161 else
162 Dbg:Alert("Undefined Action")
163 Dbg:Alert("Key: " .. cmd .. " Value: " .. cmdv)
164 end
165 else
166 Dbg:Alert("Undefined Command")
167 Dbg:Alert("Key: " .. cmd .. " Value: " .. cmdv)
168 end
169 end
170 end
171end
172
173--[[
174 LUA_ACTION.DisplayGlobals
175 Implementation of Action "Display Globals". Executed when selecting the "Display Globals" action within Composer.
176 Provided as an example for actions.
177--]]
178function LUA_ACTION.DisplayGlobals()
179 print ("Global Variables")
180 print ("----------------------------")
181
182 for k,v in pairs(_G) do -- globals`
183 if not (type(v) == "function") then
184 --print(k .. ": " .. tostring(v))
185 if (string.find(k, "^g%L") == 1) then
186 print(k .. ": " .. tostring(v))
187 if (type(v) == "table") then
188 PrintTable(v, " ")
189 end
190 end
191 end
192 end
193
194 print ("")
195end
196
197function PrintTable(tValue, sIndent)
198 sIndent = sIndent or " "
199 for k,v in pairs(tValue) do
200 print(sIndent .. tostring(k) .. ": " .. tostring(v))
201 if (type(v) == "table") then
202 PrintTable(v, sIndent .. " ")
203 end
204 end
205end
206
207function LUA_ACTION.SendSMS()
208 SendSMS()
209end
210
211function SendSMS(to, message)
212 to = to or Properties["To"]
213 message = message or Properties["Message"]
214 local postdata = "To=" .. urlencode(to) .. "&From=" .. urlencode(Properties["From"]) .. "&Body=" .. urlencode(message)
215 local username = Properties["Account SID"]
216 local password = Properties["Auth Token"]
217 local url = Properties["ServerIP"]
218 local snapurl = Properties["CameraIp"]
219
220 --[[ Create the basic authentication header ]]--
221
222 local http_headers = {}
223 --[[
224 local http_auth = "Basic " .. C4:Base64Encode(username .. ":" .. password)
225 http_headers["Authorization"] = http_auth
226 --]]
227 --[[ Set the correct content type ]]--
228 http_headers["Content-Type"] = "application/x-www-form-urlencoded"
229
230 --[[ Output the debug info ]]--
231 Dbg:Debug("Twilio API Data")
232 Dbg:Debug("---------------")
233 Dbg:Debug("To = " .. Properties["To"])
234 Dbg:Debug("From = " .. Properties["From"])
235 Dbg:Debug("Body = " .. Properties["Message"])
236 Dbg:Debug("Post Data = " .. postdata)
237 Dbg:Debug("Account SID = " .. Properties["Account SID"])
238 Dbg:Debug("Auth Token = " .. Properties["Auth Token"])
239 Dbg:Debug("URL = " .. url)
240 Dbg:Debug("URL snap = " .. snapurl)
241 --Dbg:Debug("HTTP Authentication: " .. http_auth)
242
243 --[[ Send request to twilio ]]--
244 --C4:urlPost(url, postdata, http_headers)
245 C4:urlGet(snapurl)
246end
247
248_G.strb64ff = ""
249
250
251function ReceivedAsync(idTicket, strData, errMsg, responseCode, tHeaders)
252 Dbg:Debug("Data received from server : " .. enc(strData))
253 Dbg:Debug("Server Return Code: " .. errMsg)
254 Dbg:Debug("tHeaders: "..tostring(tHeaders))
255 --Dbg:Debug("first frame: "..strb64ff)
256
257 if tostring(errMsg) == "200" then
258 local strb64 = enc(strData)
259
260 local json_data = {}
261 json_data["frame"] = strb64
262 json_data["email"] = "uxmakefiresmoke@gmail.com"
263 if strb64ff == "" then
264 --json_data["first_frame"] = strb64
265 else
266 json_data["first_frame"] = strb64ff
267 end
268 --json_data["first_frame"] = strb64
269 local result = json_encode(json_data)
270 local headers = {}
271 Dbg:Debug("result ="..result)
272 headers["Content-Type"] = "application/json"
273 headers["Content-Length"] = result:len()
274 local url = Properties["ServerIP"]
275 C4:urlPost(url, result, headers)
276 strb64ff = strb64
277
278
279
280end
281
282
283end
284
285local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -- You will need this for encoding/decoding
286-- encoding
287function enc(data)
288 return ((data:gsub('.', function(x)
289 local r,b='',x:byte()
290 for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
291 return r;
292 end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
293 if (#x < 6) then return '' end
294 local c=0
295 for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
296 return b:sub(c+1,c+1)
297 end)..({ '', '==', '=' })[#data%3+1])
298end
299
300-- decoding
301function dec(data)
302 data = string.gsub(data, '[^'..b..'=]', '')
303 return (data:gsub('.', function(x)
304 if (x == '=') then return '' end
305 local r,f='',(b:find(x)-1)
306 for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
307 return r;
308 end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
309 if (#x ~= 8) then return '' end
310 local c=0
311 for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
312 return string.char(c)
313 end))
314end
315function urlencode(str)
316 if (str) then
317 str = string.gsub (str, "\n", "\r\n")
318 str = string.gsub (str, "([^%w ])",
319 function (c) return string.format ("%%%02X", string.byte(c)) end)
320 str = string.gsub (str, " ", "+")
321 end
322 return str
323end
324
325---------------------------------------------------------------------
326-- ReceivedFromProxy Code
327---------------------------------------------------------------------
328--[[
329 ReceivedFromProxy(idBinding, sCommand, tParams)
330 Function called by Director when a proxy bound to the specified binding sends a
331 BindMessage to the DriverWorks driver.
332
333 Parameters
334 idBinding
335 Binding ID of the proxy that sent a BindMessage to the DriverWorks driver.
336 sCommand
337 Command that was sent
338 tParams
339 Lua table of received command parameters
340--]]
341function ReceivedFromProxy(idBinding, sCommand, tParams)
342 if (sCommand ~= nil) then
343 if(tParams == nil) -- initial table variable if nil
344 then tParams = {}
345 end
346 Dbg:Trace("ReceivedFromProxy(): " .. sCommand .. " on binding " .. idBinding .. "; Call Function " .. sCommand .. "()")
347 Dbg:Info(tParams)
348
349 if (PRX_CMD[sCommand]) ~= nil then
350 PRX_CMD[sCommand](idBinding, tParams)
351 else
352 Dbg:Alert("ReceivedFromProxy: Unhandled command = " .. sCommand)
353 end
354 end
355end
356
357---------------------------------------------------------------------
358-- Notification Code
359---------------------------------------------------------------------
360-- notify with parameters
361function SendNotify(notifyText, Parms, bindingID)
362 C4:SendToProxy(bindingID, notifyText, Parms, "NOTIFY")
363end
364
365-- A notify with no parameters
366function SendSimpleNotify(notifyText, ...)
367 bindingID = select(1, ...) or DEFAULT_PROXY_BINDINGID
368 C4:SendToProxy(bindingID, notifyText, {}, "NOTIFY")
369end
370
371---------------------------------------------------------------------
372-- Initialization/Destructor Code
373---------------------------------------------------------------------
374--[[
375 OnDriverInit
376 Invoked by director when a driver is loaded. This API is provided for the driver developer to contain all of the driver
377 objects that will require initialization.
378--]]
379function OnDriverInit()
380 C4:ErrorLog("INIT_CODE: OnDriverInit()")
381 -- Call all ON_DRIVER_EARLY_INIT functions.
382 for k,v in pairs(ON_DRIVER_EARLY_INIT) do
383 if (ON_DRIVER_EARLY_INIT[k] ~= nil and type(ON_DRIVER_EARLY_INIT[k]) == "function") then
384 C4:ErrorLog("INIT_CODE: ON_DRIVER_EARLY_INIT." .. k .. "()")
385 ON_DRIVER_EARLY_INIT[k]()
386 end
387 end
388
389 -- Call all ON_DRIVER_INIT functions
390 for k,v in pairs(ON_DRIVER_INIT) do
391 if (ON_DRIVER_INIT[k] ~= nil and type(ON_DRIVER_INIT[k]) == "function") then
392 C4:ErrorLog("INIT_CODE: ON_DRIVER_INIT." .. k .. "()")
393 ON_DRIVER_INIT[k]()
394 end
395 end
396
397 -- Fire OnPropertyChanged to set the initial Headers and other Property global sets, they'll change if Property is changed.
398 for k,v in pairs(Properties) do
399 OnPropertyChanged(k)
400 end
401end
402
403--[[
404 OnDriverUpdate
405 Invoked by director when an update to a driver is requested. This request can occur either by adding a new version of a driver
406 through the driver search list or right clicking on the driver and selecting "Update Driver" from within ComposerPro.
407 Its purpose is to initialize all components of the driver that are reset during a driver update.
408--]]
409function OnDriverUpdate()
410 C4:ErrorLog("INIT_CODE: OnDriverUpdate()")
411
412 -- Call all ON_DRIVER_UPDATE functions
413 for k,v in pairs(ON_DRIVER_UPDATE) do
414 if (ON_DRIVER_UPDATE[k] ~= nil and type(ON_DRIVER_UPDATE[k]) == "function") then
415 C4:ErrorLog("INIT_CODE: ON_DRIVER_UPDATE." .. k .. "()")
416 ON_DRIVER_UPDATE[k]()
417 end
418 end
419end
420
421--[[
422 OnDriverLateInit
423 Invoked by director after all drivers in the project have been loaded. This API is provided
424 for the driver developer to contain all of the driver objects that will require initialization
425 after all drivers in the project have been loaded.
426--]]
427function OnDriverLateInit()
428 C4:ErrorLog("INIT_CODE: OnDriverLateInit()")
429
430 -- Call all ON_DRIVER_LATEINIT functions
431 for k,v in pairs(ON_DRIVER_LATEINIT) do
432 if (ON_DRIVER_LATEINIT[k] ~= nil and type(ON_DRIVER_LATEINIT[k]) == "function") then
433 C4:ErrorLog("INIT_CODE: ON_DRIVER_LATEINIT." .. k .. "()")
434 ON_DRIVER_LATEINIT[k]()
435 end
436 end
437end
438
439
440--[[
441 OnDriverDestroyed
442 Function called by Director when a driver is removed. Release things this driver has allocated such as timers.
443--]]
444function OnDriverDestroyed()
445 C4:ErrorLog("INIT_CODE: OnDriverDestroyed()")
446 -- Call all ON_DRIVER_DESTROYED functions
447 for k,v in pairs(ON_DRIVER_DESTROYED) do
448 if (ON_DRIVER_DESTROYED[k] ~= nil and type(ON_DRIVER_DESTROYED[k]) == "function") then
449 C4:ErrorLog("INIT_CODE: ON_DRIVER_DESTROYED." .. k .. "()")
450 ON_DRIVER_DESTROYED[k]()
451 end
452 end
453end
454
455--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
456-- Debug Logging Code
457--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
458Log = {}
459
460-- Create a Table with Logging functions
461function Log:Create()
462
463 -- table for logging functions
464 local lt = {}
465
466 lt._logLevel = 0
467 lt._outputPrint = false
468 lt._outputC4Log = false
469 lt._logName = "Set Log Name to display"
470
471 function lt:SetLogLevel(level)
472 self._logLevel = level
473 end
474
475 function lt:OutputPrint(value)
476 self._outputPrint = value
477 end
478
479 function lt:OutputC4Log(value)
480 self._outputC4Log = value
481 end
482
483 function lt:SetLogName(name)
484 self._logName = name
485 end
486
487 function lt:Enabled()
488 return (self._outputPrint or self._outputC4Log)
489 end
490
491 function lt:PrintTable(tValue, sIndent)
492 if (type(tValue) == "table") then
493 if (self._outputPrint) then
494 for k,v in pairs(tValue) do
495 print(sIndent .. tostring(k) .. ": " .. tostring(v))
496 if (type(v) == "table") then
497 self:PrintTable(v, sIndent .. " ")
498 end
499 end
500 end
501
502 if (self._outputC4Log) then
503 for k,v in pairs(tValue) do
504 C4:ErrorLog(self._logName .. ": " .. sIndent .. tostring(k) .. ": " .. tostring(v))
505 if (type(v) == "table") then
506 self:PrintTable(v, sIndent .. " ")
507 end
508 end
509 end
510
511 else
512 if (self._outputPrint) then
513 print (sIndent .. tValue)
514 end
515
516 if (self._outputC4Log) then
517 C4:ErrorLog(self._logName .. ": " .. sIndent .. tValue)
518 end
519 end
520 end
521
522 function lt:Print(logLevel, sLogText)
523 if (self._logLevel >= logLevel) then
524 if (type(sLogText) == "table") then
525 self:PrintTable(sLogText, " ")
526 return
527 end
528
529 if (self._outputPrint) then
530 print (sLogText)
531 end
532
533 if (self._outputC4Log) then
534 C4:ErrorLog(self._logName .. ": " .. sLogText)
535 end
536 end
537 end
538
539 function lt:Alert(strDebugText)
540 self:Print(0, strDebugText)
541 end
542
543 function lt:Error(strDebugText)
544 self:Print(1, strDebugText)
545 end
546
547 function lt:Warn(strDebugText)
548 self:Print(2, strDebugText)
549 end
550
551 function lt:Info(strDebugText)
552 self:Print(3, strDebugText)
553 end
554
555 function lt:Trace(strDebugText)
556 self:Print(4, strDebugText)
557 end
558
559 function lt:Debug(strDebugText)
560 self:Print(5, strDebugText)
561 end
562
563 return lt
564end
565
566function ON_DRIVER_EARLY_INIT.LogLib()
567 -- Create and initialize debug logging
568 Dbg = Log.Create()
569 Dbg:SetLogName("base_template PLEASE CHANGE")
570end
571
572function ON_DRIVER_INIT.LogLib()
573 -- Create Debug Timer
574 gDebugTimer = Timer:Create("Debug", 45, "MINUTES", OnDebugTimerExpired)
575end
576
577--[[
578 OnDebugTimerExpired
579 Debug timer callback function
580--]]
581function OnDebugTimerExpired()
582 Dbg:Warn("Turning Debug Mode Off (timer expired)")
583 gDebugTimer:KillTimer()
584 C4:UpdateProperty("Debug Mode", "Off")
585 OnPropertyChanged("Debug Mode")
586end
587
588---------------------------------------------------------------------
589-- Timer Code
590---------------------------------------------------------------------
591Timer = {}
592
593-- Create a Table with Timer functions
594function Timer:Create(name, interval, units, Callback, repeating, Info)
595 -- timers table
596 local tt = {}
597
598 tt._name = name
599 tt._timerID = TimerLibGetNextTimerID()
600 tt._interval = interval
601 tt._units = units
602 tt._repeating = repeating or false
603 tt._Callback = Callback
604 tt._info = Info or ""
605 tt._id = 0
606
607 function tt:StartTimer(...)
608 self:KillTimer()
609
610 -- optional parameters (interval, units, repeating)
611 if ... then
612 local interval = select(1, ...)
613 local units = select(2, ...)
614 local repeating = select(3, ...)
615
616 self._interval = interval or self._interval
617 self._units = units or self._units
618 self._repeating = repeating or self._repeating
619 end
620
621 if (self._interval > 0) then
622 Dbg:Trace("Starting Timer: " .. self._name)
623 self._id = C4:AddTimer(self._interval, self._units, self._repeating)
624 end
625 end
626
627 function tt:KillTimer()
628 if (self._id) then
629 self._id = C4:KillTimer(self._id)
630 end
631 end
632
633 function tt:TimerStarted()
634 return (self._id ~= 0)
635 end
636
637 function tt:TimerStopped()
638 return not self:TimerStarted()
639 end
640
641 gTimerLibTimers[tt._timerID] = tt
642 Dbg:Trace("Created timer " .. tt._name)
643
644 return tt
645end
646
647function TimerLibGetNextTimerID()
648 gTimerLibTimerCurID = gTimerLibTimerCurID + 1
649 return gTimerLibTimerCurID
650end
651
652function ON_DRIVER_EARLY_INIT.TimerLib()
653 gTimerLibTimers = {}
654 gTimerLibTimerCurID = 0
655end
656
657function ON_DRIVER_DESTROYED.TimerLib()
658 -- Kill open timers
659 for k,v in pairs(gTimerLibTimers) do
660 v:KillTimer()
661 end
662end
663
664--[[
665 OnTimerExpired
666 Function called by Director when the specified Control4 timer expires.
667 Parameters
668 idTimer
669 Timer ID of expired timer.
670--]]
671function OnTimerExpired(idTimer)
672 for k,v in pairs(gTimerLibTimers) do
673 if (idTimer == v._id) then
674 if (v._Callback) then
675 v._Callback(v._info)
676 end
677 end
678 end
679end
680
681
682
683
684
685
686
687
688
689
690--kopirana biblioteka
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707local encode
708
709local escape_char_map = {
710 [ "\\" ] = "\\\\",
711 [ "\"" ] = "\\\"",
712 [ "\b" ] = "\\b",
713 [ "\f" ] = "\\f",
714 [ "\n" ] = "\\n",
715 [ "\r" ] = "\\r",
716 [ "\t" ] = "\\t",
717}
718
719local escape_char_map_inv = { [ "\\/" ] = "/" }
720for k, v in pairs(escape_char_map) do
721 escape_char_map_inv[v] = k
722end
723
724
725local function escape_char(c)
726 return escape_char_map[c] or string.format("\\u%04x", c:byte())
727end
728
729
730local function encode_nil(val)
731 return "null"
732end
733
734
735local function encode_table(val, stack)
736 local res = {}
737 stack = stack or {}
738
739 -- Circular reference?
740 if stack[val] then error("circular reference") end
741
742 stack[val] = true
743
744 if val[1] ~= nil or next(val) == nil then
745 -- Treat as array -- check keys are valid and it is not sparse
746 local n = 0
747 for k in pairs(val) do
748 if type(k) ~= "number" then
749 error("invalid table: mixed or invalid key types")
750 end
751 n = n + 1
752 end
753 if n ~= #val then
754 error("invalid table: sparse array")
755 end
756 -- Encode
757 for i, v in ipairs(val) do
758 table.insert(res, encode(v, stack))
759 end
760 stack[val] = nil
761 return "[" .. table.concat(res, ",") .. "]"
762
763 else
764 -- Treat as an object
765 for k, v in pairs(val) do
766 if type(k) ~= "string" then
767 error("invalid table: mixed or invalid key types")
768 end
769 table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
770 end
771 stack[val] = nil
772 return "{" .. table.concat(res, ",") .. "}"
773 end
774end
775
776
777local function encode_string(val)
778 return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
779end
780
781--function encode_string(val)
782-- return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
783--end
784
785
786local function encode_number(val)
787 -- Check for NaN, -inf and inf
788 if val ~= val or val <= -math.huge or val >= math.huge then
789 error("unexpected number value '" .. tostring(val) .. "'")
790 end
791 return string.format("%.14g", val)
792end
793
794
795local type_func_map = {
796 [ "nil" ] = encode_nil,
797 [ "table" ] = encode_table,
798 [ "string" ] = encode_string,
799 [ "number" ] = encode_number,
800 [ "boolean" ] = tostring,
801}
802
803
804encode = function(val, stack)
805 local t = type(val)
806 local f = type_func_map[t]
807 if f then
808 return f(val, stack)
809 end
810 error("unexpected type '" .. t .. "'")
811end
812
813
814function json_encode(val)
815 return ( encode(val) )
816end
817
818
819-------------------------------------------------------------------------------
820-- Decode
821-------------------------------------------------------------------------------
822
823local parse
824
825local function create_set(...)
826 local res = {}
827 for i = 1, select("#", ...) do
828 res[ select(i, ...) ] = true
829 end
830 return res
831end
832
833local space_chars = create_set(" ", "\t", "\r", "\n")
834local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
835local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
836local literals = create_set("true", "false", "null")
837
838local literal_map = {
839 [ "true" ] = true,
840 [ "false" ] = false,
841 [ "null" ] = nil,
842}
843
844
845local function next_char(str, idx, set, negate)
846 for i = idx, #str do
847 if set[str:sub(i, i)] ~= negate then
848 return i
849 end
850 end
851 return #str + 1
852end
853
854
855local function decode_error(str, idx, msg)
856 local line_count = 1
857 local col_count = 1
858 for i = 1, idx - 1 do
859 col_count = col_count + 1
860 if str:sub(i, i) == "\n" then
861 line_count = line_count + 1
862 col_count = 1
863 end
864 end
865 error( string.format("%s at line %d col %d", msg, line_count, col_count) )
866end
867
868
869local function codepoint_to_utf8(n)
870 -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
871 local f = math.floor
872 if n <= 0x7f then
873 return string.char(n)
874 elseif n <= 0x7ff then
875 return string.char(f(n / 64) + 192, n % 64 + 128)
876 elseif n <= 0xffff then
877 return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
878 elseif n <= 0x10ffff then
879 return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
880 f(n % 4096 / 64) + 128, n % 64 + 128)
881 end
882 error( string.format("invalid unicode codepoint '%x'", n) )
883end
884
885
886local function parse_unicode_escape(s)
887 local n1 = tonumber( s:sub(3, 6), 16 )
888 local n2 = tonumber( s:sub(9, 12), 16 )
889 -- Surrogate pair?
890 if n2 then
891 return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
892 else
893 return codepoint_to_utf8(n1)
894 end
895end
896
897
898local function parse_string(str, i)
899 local has_unicode_escape = false
900 local has_surrogate_escape = false
901 local has_escape = false
902 local last
903 for j = i + 1, #str do
904 local x = str:byte(j)
905
906 if x < 32 then
907 decode_error(str, j, "control character in string")
908 end
909
910 if last == 92 then -- "\\" (escape char)
911 if x == 117 then -- "u" (unicode escape sequence)
912 local hex = str:sub(j + 1, j + 5)
913 if not hex:find("%x%x%x%x") then
914 decode_error(str, j, "invalid unicode escape in string")
915 end
916 if hex:find("^[dD][89aAbB]") then
917 has_surrogate_escape = true
918 else
919 has_unicode_escape = true
920 end
921 else
922 local c = string.char(x)
923 if not escape_chars[c] then
924 decode_error(str, j, "invalid escape char '" .. c .. "' in string")
925 end
926 has_escape = true
927 end
928 last = nil
929
930 elseif x == 34 then -- '"' (end of string)
931 local s = str:sub(i + 1, j - 1)
932 if has_surrogate_escape then
933 s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
934 end
935 if has_unicode_escape then
936 s = s:gsub("\\u....", parse_unicode_escape)
937 end
938 if has_escape then
939 s = s:gsub("\\.", escape_char_map_inv)
940 end
941 return s, j + 1
942
943 else
944 last = x
945 end
946 end
947 decode_error(str, i, "expected closing quote for string")
948end
949
950
951local function parse_number(str, i)
952 local x = next_char(str, i, delim_chars)
953 local s = str:sub(i, x - 1)
954 local n = tonumber(s)
955 if not n then
956 decode_error(str, i, "invalid number '" .. s .. "'")
957 end
958 return n, x
959end
960
961
962local function parse_literal(str, i)
963 local x = next_char(str, i, delim_chars)
964 local word = str:sub(i, x - 1)
965 if not literals[word] then
966 decode_error(str, i, "invalid literal '" .. word .. "'")
967 end
968 return literal_map[word], x
969end
970
971
972local function parse_array(str, i)
973 local res = {}
974 local n = 1
975 i = i + 1
976 while 1 do
977 local x
978 i = next_char(str, i, space_chars, true)
979 -- Empty / end of array?
980 if str:sub(i, i) == "]" then
981 i = i + 1
982 break
983 end
984 -- Read token
985 x, i = parse(str, i)
986 res[n] = x
987 n = n + 1
988 -- Next token
989 i = next_char(str, i, space_chars, true)
990 local chr = str:sub(i, i)
991 i = i + 1
992 if chr == "]" then break end
993 if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
994 end
995 return res, i
996end
997
998
999local function parse_object(str, i)
1000 local res = {}
1001 i = i + 1
1002 while 1 do
1003 local key, val
1004 i = next_char(str, i, space_chars, true)
1005 -- Empty / end of object?
1006 if str:sub(i, i) == "}" then
1007 i = i + 1
1008 break
1009 end
1010 -- Read key
1011 if str:sub(i, i) ~= '"' then
1012 decode_error(str, i, "expected string for key")
1013 end
1014 key, i = parse(str, i)
1015 -- Read ':' delimiter
1016 i = next_char(str, i, space_chars, true)
1017 if str:sub(i, i) ~= ":" then
1018 decode_error(str, i, "expected ':' after key")
1019 end
1020 i = next_char(str, i + 1, space_chars, true)
1021 -- Read value
1022 val, i = parse(str, i)
1023 -- Set
1024 res[key] = val
1025 -- Next token
1026 i = next_char(str, i, space_chars, true)
1027 local chr = str:sub(i, i)
1028 i = i + 1
1029 if chr == "}" then break end
1030 if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
1031 end
1032 return res, i
1033end
1034
1035
1036local char_func_map = {
1037 [ '"' ] = parse_string,
1038 [ "0" ] = parse_number,
1039 [ "1" ] = parse_number,
1040 [ "2" ] = parse_number,
1041 [ "3" ] = parse_number,
1042 [ "4" ] = parse_number,
1043 [ "5" ] = parse_number,
1044 [ "6" ] = parse_number,
1045 [ "7" ] = parse_number,
1046 [ "8" ] = parse_number,
1047 [ "9" ] = parse_number,
1048 [ "-" ] = parse_number,
1049 [ "t" ] = parse_literal,
1050 [ "f" ] = parse_literal,
1051 [ "n" ] = parse_literal,
1052 [ "[" ] = parse_array,
1053 [ "{" ] = parse_object,
1054}
1055
1056
1057parse = function(str, idx)
1058 local chr = str:sub(idx, idx)
1059 local f = char_func_map[chr]
1060 if f then
1061 return f(str, idx)
1062 end
1063 decode_error(str, idx, "unexpected character '" .. chr .. "'")
1064end
1065function json_decode(str)
1066 if type(str) ~= "string" then
1067 error("expected argument of type string, got " .. type(str))
1068 end
1069 local res, idx = parse(str, next_char(str, 1, space_chars, true))
1070 idx = next_char(str, idx, space_chars, true)
1071 if idx <= #str then
1072 decode_error(str, idx, "trailing garbage")
1073 end
1074 return res
1075end