GEPRC Dual Diversity 2.4GHz RX (#2265)
[ExpressLRS.git] / src / lua / elrsV3.lua
blob3391340afeb60a71e93c4105f37194d86cbd956e
1 -- TNS|ExpressLRS|TNE
2 ---- #########################################################################
3 ---- # #
4 ---- # Copyright (C) OpenTX #
5 -----# #
6 ---- # License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html #
7 ---- # #
8 ---- # This program is free software; you can redistribute it and/or modify #
9 ---- # it under the terms of the GNU General Public License version 2 as #
10 ---- # published by the Free Software Foundation. #
11 ---- # #
12 ---- # This program is distributed in the hope that it will be useful #
13 ---- # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 ---- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 ---- # GNU General Public License for more details. #
16 ---- # #
17 ---- #########################################################################
18 local deviceId = 0xEE
19 local handsetId = 0xEF
20 local deviceName = ""
21 local lineIndex = 1
22 local pageOffset = 0
23 local edit = nil
24 local charIndex = 1
25 local fieldPopup
26 local fieldTimeout = 0
27 local loadQ = {}
28 local fieldChunk = 0
29 local fieldData = {}
30 local fields = {}
31 local devices = {}
32 local goodBadPkt = "?/??? ?"
33 local elrsFlags = 0
34 local elrsFlagsInfo = ""
35 local fields_count = 0
36 local backButtonId = 2
37 local exitButtonId = 3
38 local devicesRefreshTimeout = 50
39 local folderAccess = nil
40 local commandRunningIndicator = 1
41 local expectChunksRemain = -1
42 local deviceIsELRS_TX = nil
43 local linkstatTimeout = 100
44 local titleShowWarn = nil
45 local titleShowWarnTimeout = 100
46 local exitscript = 0
48 local COL2
49 local maxLineIndex
50 local textXoffset
51 local textYoffset
52 local textSize
53 local byteToStr
55 local function allocateFields()
56 fields = {}
57 for i=1, fields_count + 2 + #devices do
58 fields[i] = { }
59 end
60 backButtonId = fields_count + 2 + #devices
61 fields[backButtonId] = {name="----BACK----", parent = 255, type=14}
62 if folderAccess ~= nil then
63 fields[backButtonId].parent = folderAccess
64 end
65 exitButtonId = backButtonId + 1
66 fields[exitButtonId] = {id = exitButtonId, name="----EXIT----", type=17}
67 end
69 local function reloadAllField()
70 fieldChunk = 0
71 fieldData = {}
72 -- loadQ is actually a stack
73 loadQ = {}
74 for fieldId = fields_count, 1, -1 do
75 loadQ[#loadQ+1] = fieldId
76 end
77 end
79 local function getField(line)
80 local counter = 1
81 for i = 1, #fields do
82 local field = fields[i]
83 if folderAccess == field.parent and not field.hidden then
84 if counter < line then
85 counter = counter + 1
86 else
87 return field
88 end
89 end
90 end
91 end
93 local function constrain(x, low, high)
94 if x < low then
95 return low
96 elseif x > high then
97 return high
98 end
99 return x
102 -- Change display attribute to current field
103 local function incrField(step)
104 local field = getField(lineIndex)
105 local min, max = 0, 0
106 if ((field.type <= 5) or (field.type == 8)) then
107 min = field.min or 0
108 max = field.max or 0
109 step = field.step * step
110 elseif field.type == 9 then
111 min = 0
112 max = #field.values - 1
114 field.value = constrain(field.value + step, min, max)
117 -- Select the next or previous editable field
118 local function selectField(step)
119 local newLineIndex = lineIndex
120 local field
121 repeat
122 newLineIndex = newLineIndex + step
123 if newLineIndex <= 0 then
124 newLineIndex = #fields
125 elseif newLineIndex == 1 + #fields then
126 newLineIndex = 1
127 pageOffset = 0
129 field = getField(newLineIndex)
130 until newLineIndex == lineIndex or (field and field.name)
131 lineIndex = newLineIndex
132 if lineIndex > maxLineIndex + pageOffset then
133 pageOffset = lineIndex - maxLineIndex
134 elseif lineIndex <= pageOffset then
135 pageOffset = lineIndex - 1
139 local function fieldGetSelectOpts(data, offset, last)
140 if last then
141 while data[offset] ~= 0 do
142 offset = offset + 1
144 return last, offset + 1
147 -- Split a table of byte values (string) with ; separator into a table
148 local r = {}
149 local opt = ''
150 local b = data[offset]
151 while b ~= 0 do
152 if b == 59 then -- ';'
153 r[#r+1] = opt
154 opt = ''
155 else
156 opt = opt .. byteToStr(b)
158 offset = offset + 1
159 b = data[offset]
162 r[#r+1] = opt
163 opt = nil
164 return r, offset + 1, collectgarbage("collect")
167 local function fieldGetString(data, offset, last)
168 if last then
169 return last, offset + #last + 1
172 local result = ""
173 while data[offset] ~= 0 do
174 result = result .. byteToStr(data[offset])
175 offset = offset + 1
178 return result, offset + 1, collectgarbage("collect")
181 local function getDevice(name)
182 for i=1, #devices do
183 if devices[i].name == name then
184 return devices[i]
189 local function fieldGetValue(data, offset, size)
190 local result = 0
191 for i=0, size-1 do
192 result = bit32.lshift(result, 8) + data[offset + i]
194 return result
197 local function fieldUnsignedLoad(field, data, offset, size)
198 field.value = fieldGetValue(data, offset, size)
199 field.min = fieldGetValue(data, offset+size, size)
200 field.max = fieldGetValue(data, offset+2*size, size)
201 --field.default = fieldGetValue(data, offset+3*size, size)
202 field.unit = fieldGetString(data, offset+4*size, field.unit)
203 field.step = 1
206 local function fieldUnsignedToSigned(field, size)
207 local bandval = bit32.lshift(0x80, (size-1)*8)
208 field.value = field.value - bit32.band(field.value, bandval) * 2
209 field.min = field.min - bit32.band(field.min, bandval) * 2
210 field.max = field.max - bit32.band(field.max, bandval) * 2
211 --field.default = field.default - bit32.band(field.default, bandval) * 2
214 local function fieldSignedLoad(field, data, offset, size)
215 fieldUnsignedLoad(field, data, offset, size)
216 fieldUnsignedToSigned(field, size)
219 local function fieldIntSave(index, value, size)
220 local frame = { deviceId, handsetId, index }
221 for i=size-1, 0, -1 do
222 frame[#frame + 1] = (bit32.rshift(value, 8*i) % 256)
224 crossfireTelemetryPush(0x2D, frame)
227 local function fieldUnsignedSave(field, size)
228 local value = field.value
229 fieldIntSave(field.id, value, size)
232 local function fieldSignedSave(field, size)
233 local value = field.value
234 if value < 0 then
235 value = bit32.lshift(0x100, (size-1)*8) + value
237 fieldIntSave(field.id, value, size)
240 local function fieldIntDisplay(field, y, attr)
241 lcd.drawText(COL2, y, field.value .. field.unit, attr)
244 -- UINT8
245 local function fieldUint8Load(field, data, offset)
246 fieldUnsignedLoad(field, data, offset, 1)
249 local function fieldUint8Save(field)
250 fieldUnsignedSave(field, 1)
253 -- INT8
254 local function fieldInt8Load(field, data, offset)
255 fieldSignedLoad(field, data, offset, 1)
258 local function fieldInt8Save(field)
259 fieldSignedSave(field, 1)
262 -- UINT16
263 local function fieldUint16Load(field, data, offset)
264 fieldUnsignedLoad(field, data, offset, 2)
267 local function fieldUint16Save(field)
268 fieldUnsignedSave(field, 2)
271 -- INT16
272 local function fieldInt16Load(field, data, offset)
273 fieldSignedLoad(field, data, offset, 2)
276 local function fieldInt16Save(field)
277 fieldSignedSave(field, 2)
280 -- TEXT SELECTION
281 local function fieldTextSelectionLoad(field, data, offset)
282 field.values, offset = fieldGetSelectOpts(data, offset, field.nc == nil and field.values)
283 field.value = data[offset]
284 -- min max and default (offset+1 to 3) are not used on selections
285 -- units never uses cache
286 field.unit = fieldGetString(data, offset+4)
287 field.nc = nil -- use cache next time
290 local function fieldTextSelectionSave(field)
291 crossfireTelemetryPush(0x2D, { deviceId, handsetId, field.id, field.value })
294 local function fieldTextSelectionDisplay_color(field, y, attr)
295 local val = field.values[field.value+1] or "ERR"
296 lcd.drawText(COL2, y, val, attr)
297 local strPix = lcd.sizeText and lcd.sizeText(val) or (10 * #val)
298 lcd.drawText(COL2 + strPix, y, field.unit, 0)
301 local function fieldTextSelectionDisplay_bw(field, y, attr)
302 lcd.drawText(COL2, y, field.values[field.value+1] or "ERR", attr)
303 lcd.drawText(lcd.getLastPos(), y, field.unit, 0)
306 -- STRING
307 local function fieldStringLoad(field, data, offset)
308 field.value, offset = fieldGetString(data, offset)
309 if #data >= offset then
310 field.maxlen = data[offset]
314 local function fieldStringDisplay(field, y, attr)
315 lcd.drawText(COL2, y, field.value, attr)
318 local function fieldFolderOpen(field)
319 folderAccess = field.id
320 local backFld = fields[backButtonId]
321 -- Store the lineIndex and pageOffset to return to in the backFld
322 backFld.li = lineIndex
323 backFld.po = pageOffset
324 backFld.parent = folderAccess
326 lineIndex = 1
327 pageOffset = 0
330 local function fieldFolderDeviceOpen(field)
331 crossfireTelemetryPush(0x28, { 0x00, 0xEA }) --broadcast with standard handset ID to get all node respond correctly
332 return fieldFolderOpen(field)
335 local function fieldFolderDisplay(field,y ,attr)
336 lcd.drawText(textXoffset, y, "> " .. field.name, bit32.bor(attr, BOLD))
339 local function fieldCommandLoad(field, data, offset)
340 field.status = data[offset]
341 field.timeout = data[offset+1]
342 field.info = fieldGetString(data, offset+2)
343 if field.status == 0 then
344 fieldPopup = nil
348 local function fieldCommandSave(field)
349 if field.status ~= nil then
350 if field.status < 4 then
351 field.status = 1
352 crossfireTelemetryPush(0x2D, { deviceId, handsetId, field.id, field.status })
353 fieldPopup = field
354 fieldPopup.lastStatus = 0
355 commandRunningIndicator = 1
356 fieldTimeout = getTime() + field.timeout
361 local function fieldCommandDisplay(field, y, attr)
362 lcd.drawText(10, y, "[" .. field.name .. "]", bit32.bor(attr, BOLD))
365 local function UIbackExec()
366 local backFld = fields[backButtonId]
367 lineIndex = backFld.li or 1
368 pageOffset = backFld.po or 0
370 backFld.parent = 255
371 backFld.li = nil
372 backFld.po = nil
373 folderAccess = nil
376 local function UIexitExec()
377 exitscript = 1
380 local function changeDeviceId(devId) --change to selected device ID
381 folderAccess = nil
382 deviceIsELRS_TX = nil
383 elrsFlags = 0
384 --if the selected device ID (target) is a TX Module, we use our Lua ID, so TX Flag that user is using our LUA
385 if devId == 0xEE then
386 handsetId = 0xEF
387 else --else we would act like the legacy lua
388 handsetId = 0xEA
390 deviceId = devId
391 fields_count = 0 --set this because next target wouldn't have the same count, and this trigger to request the new count
394 local function fieldDeviceIdSelect(field)
395 local device = getDevice(field.name)
396 changeDeviceId(device.id)
397 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
400 local function createDeviceFields() -- put other devices in the field list
401 fields[fields_count + 2 + #devices] = fields[backButtonId]
402 backButtonId = fields_count + 2 + #devices -- move back button to the end of the list, so it will always show up at the bottom.
403 for i=1, #devices do
404 if devices[i].id == deviceId then
405 fields[fields_count+1+i] = {name=devices[i].name, parent = 255, type=15}
406 else
407 fields[fields_count+1+i] = {name=devices[i].name, parent = fields_count+1, type=15}
412 local function parseDeviceInfoMessage(data)
413 local offset
414 local id = data[2]
415 local newName
416 newName, offset = fieldGetString(data, 3)
417 local device = getDevice(newName)
418 if device == nil then
419 device = { id = id, name = newName }
420 devices[#devices + 1] = device
422 if deviceId == id then
423 deviceName = newName
424 deviceIsELRS_TX = ((fieldGetValue(data,offset,4) == 0x454C5253) and (deviceId == 0xEE)) or nil -- SerialNumber = 'E L R S' and ID is TX module
425 local newFieldCount = data[offset+12]
426 if newFieldCount ~= fields_count or newFieldCount == 0 then
427 fields_count = newFieldCount
428 allocateFields()
429 reloadAllField()
430 fields[fields_count+1] = {id = fields_count+1, name="Other Devices", parent = 255, type=16} -- add other devices folders
431 if newFieldCount == 0 then
432 -- This device has no fields so the Loading code never starts
433 createDeviceFields()
439 local functions = {
440 { load=fieldUint8Load, save=fieldUint8Save, display=fieldIntDisplay }, --1 UINT8(0)
441 { load=fieldInt8Load, save=fieldInt8Save, display=fieldIntDisplay }, --2 INT8(1)
442 { load=fieldUint16Load, save=fieldUint16Save, display=fieldIntDisplay }, --3 UINT16(2)
443 { load=fieldInt16Load, save=fieldInt16Save, display=fieldIntDisplay }, --4 INT16(3)
444 nil,
445 nil,
446 nil,
447 nil,
448 nil, --9 FLOAT(8)
449 { load=fieldTextSelectionLoad, save=fieldTextSelectionSave, display = nil }, --10 SELECT(9)
450 { load=fieldStringLoad, save=nil, display=fieldStringDisplay }, --11 STRING(10) editing NOTIMPL
451 { load=nil, save=fieldFolderOpen, display=fieldFolderDisplay }, --12 FOLDER(11)
452 { load=fieldStringLoad, save=nil, display=fieldStringDisplay }, --13 INFO(12)
453 { load=fieldCommandLoad, save=fieldCommandSave, display=fieldCommandDisplay }, --14 COMMAND(13)
454 { load=nil, save=UIbackExec, display=fieldCommandDisplay }, --15 back(14)
455 { load=nil, save=fieldDeviceIdSelect, display=fieldCommandDisplay }, --16 device(15)
456 { load=nil, save=fieldFolderDeviceOpen, display=fieldFolderDisplay }, --17 deviceFOLDER(16)
457 { load=nil, save=UIexitExec, display=fieldCommandDisplay }, --18 exit(17)
460 local function parseParameterInfoMessage(data)
461 local fieldId = (fieldPopup and fieldPopup.id) or loadQ[#loadQ]
462 if data[2] ~= deviceId or data[3] ~= fieldId then
463 fieldData = {}
464 fieldChunk = 0
465 return
467 local field = fields[fieldId]
468 local chunksRemain = data[4]
469 -- If no field or the chunksremain changed when we have data, don't continue
470 if not field or (chunksRemain ~= expectChunksRemain and #fieldData ~= 0) then
471 return
473 expectChunksRemain = chunksRemain - 1
474 for i=5, #data do
475 fieldData[#fieldData + 1] = data[i]
477 if chunksRemain > 0 then
478 fieldChunk = fieldChunk + 1
479 else
480 loadQ[#loadQ] = nil
481 -- Populate field from fieldData
482 if #fieldData > 3 then
483 local offset
484 field.id = fieldId
485 field.parent = (fieldData[1] ~= 0) and fieldData[1] or nil
486 field.type = bit32.band(fieldData[2], 0x7f)
487 field.hidden = bit32.btest(fieldData[2], 0x80) or nil
488 field.name, offset = fieldGetString(fieldData, 3, field.name)
489 if functions[field.type+1].load then
490 functions[field.type+1].load(field, fieldData, offset)
492 if field.min == 0 then field.min = nil end
493 if field.max == 0 then field.max = nil end
496 fieldChunk = 0
497 fieldData = {}
499 -- Last field loaded, add the list of devices to the end
500 if #loadQ == 0 then
501 createDeviceFields()
504 -- Return value is if the screen should be updated
505 -- If deviceId is TX module, then the Bad/Good drives the update; for other
506 -- devices update each new item. and always update when the queue empties
507 return deviceId ~= 0xEE or #loadQ == 0
511 local function parseElrsInfoMessage(data)
512 if data[2] ~= deviceId then
513 fieldData = {}
514 fieldChunk = 0
515 return
518 local badPkt = data[3]
519 local goodPkt = (data[4]*256) + data[5]
520 local newFlags = data[6]
521 -- If flags are changing, reset the warning timeout to display/hide message immediately
522 if newFlags ~= elrsFlags then
523 elrsFlags = newFlags
524 titleShowWarnTimeout = 0
526 elrsFlagsInfo = fieldGetString(data, 7)
528 local state = (bit32.btest(elrsFlags, 1) and "C") or "-"
529 goodBadPkt = string.format("%u/%u %s", badPkt, goodPkt, state)
532 local function parseElrsV1Message(data)
533 if (data[1] ~= 0xEA) or (data[2] ~= 0xEE) then
534 return
537 -- local badPkt = data[9]
538 -- local goodPkt = (data[10]*256) + data[11]
539 -- goodBadPkt = string.format("%u/%u X", badPkt, goodPkt)
540 fieldPopup = {id = 0, status = 2, timeout = 0xFF, info = "ERROR: 1.x firmware"}
541 fieldTimeout = getTime() + 0xFFFF
544 local function refreshNext()
545 local command, data, forceRedraw
546 repeat
547 command, data = crossfireTelemetryPop()
548 if command == 0x29 then
549 parseDeviceInfoMessage(data)
550 elseif command == 0x2B then
551 if parseParameterInfoMessage(data) then
552 forceRedraw = true
554 if #loadQ > 0 then
555 fieldTimeout = 0 -- request next chunk immediately
556 elseif fieldPopup then
557 fieldTimeout = getTime() + fieldPopup.timeout
559 elseif command == 0x2D then
560 parseElrsV1Message(data)
561 elseif command == 0x2E then
562 parseElrsInfoMessage(data)
563 forceRedraw = true
565 until command == nil
567 local time = getTime()
568 if fieldPopup then
569 if time > fieldTimeout and fieldPopup.status ~= 3 then
570 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 6 }) -- lcsQuery
571 fieldTimeout = time + fieldPopup.timeout
573 elseif time > devicesRefreshTimeout and fields_count < 1 then
574 forceRedraw = true -- handles initial screen draw
575 devicesRefreshTimeout = time + 100 -- 1s
576 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
577 elseif time > linkstatTimeout then
578 if not deviceIsELRS_TX and #loadQ == 0 then
579 goodBadPkt = ""
580 else
581 crossfireTelemetryPush(0x2D, { deviceId, handsetId, 0x0, 0x0 }) --request linkstat
583 linkstatTimeout = time + 100
584 elseif time > fieldTimeout and fields_count ~= 0 then
585 if #loadQ > 0 then
586 crossfireTelemetryPush(0x2C, { deviceId, handsetId, loadQ[#loadQ], fieldChunk })
587 fieldTimeout = time + 50 -- 0.5s
591 if time > titleShowWarnTimeout then
592 -- if elrsFlags bit set is bit higher than bit 0 and bit 1, it is warning flags
593 titleShowWarn = (elrsFlags > 3 and not titleShowWarn) or nil
594 titleShowWarnTimeout = time + 100
595 forceRedraw = true
598 return forceRedraw
601 local lcd_title -- holds function that is color/bw version
602 local function lcd_title_color()
603 lcd.clear()
605 local EBLUE = lcd.RGB(0x43, 0x61, 0xAA)
606 local EGREEN = lcd.RGB(0x9f, 0xc7, 0x6f)
607 local EGREY1 = lcd.RGB(0x91, 0xb2, 0xc9)
608 local EGREY2 = lcd.RGB(0x6f, 0x62, 0x7f)
609 local barHeight = 30
611 -- Field display area (white w/ 2px green border)
612 lcd.setColor(CUSTOM_COLOR, EGREEN)
613 lcd.drawRectangle(0, 0, LCD_W, LCD_H, CUSTOM_COLOR)
614 lcd.drawRectangle(1, 0, LCD_W - 2, LCD_H - 1, CUSTOM_COLOR)
615 -- title bar
616 lcd.drawFilledRectangle(0, 0, LCD_W, barHeight, CUSTOM_COLOR)
617 lcd.setColor(CUSTOM_COLOR, EGREY1)
618 lcd.drawFilledRectangle(LCD_W - textSize, 0, textSize, barHeight, CUSTOM_COLOR)
619 lcd.setColor(CUSTOM_COLOR, EGREY2)
620 lcd.drawRectangle(LCD_W - textSize, 0, textSize, barHeight - 1, CUSTOM_COLOR)
621 lcd.drawRectangle(LCD_W - textSize, 1 , textSize - 1, barHeight - 2, CUSTOM_COLOR) -- left and bottom line only 1px, make it look bevelled
622 lcd.setColor(CUSTOM_COLOR, BLACK)
623 if titleShowWarn then
624 lcd.drawText(textXoffset + 1, 4, elrsFlagsInfo, CUSTOM_COLOR)
625 else
626 local title = fields_count > 0 and deviceName or "Loading..."
627 lcd.drawText(textXoffset + 1, 4, title, CUSTOM_COLOR)
628 lcd.drawText(LCD_W - 5, 4, goodBadPkt, RIGHT + BOLD + CUSTOM_COLOR)
630 -- progress bar
631 if #loadQ > 0 and fields_count > 0 then
632 local barW = (COL2-4) * (fields_count - #loadQ) / fields_count
633 lcd.setColor(CUSTOM_COLOR, EBLUE)
634 lcd.drawFilledRectangle(2, 2+20, barW, barHeight-5-20, CUSTOM_COLOR)
635 lcd.setColor(CUSTOM_COLOR, WHITE)
636 lcd.drawFilledRectangle(2+barW, 2+20, COL2-2-barW, barHeight-5-20, CUSTOM_COLOR)
640 local function lcd_title_bw()
641 lcd.clear()
642 -- B&W screen
643 local barHeight = 9
644 if not titleShowWarn then
645 lcd.drawText(LCD_W - 1, 1, goodBadPkt, RIGHT)
646 lcd.drawLine(LCD_W - 10, 0, LCD_W - 10, barHeight-1, SOLID, INVERS)
649 if #loadQ > 0 and fields_count > 0 then
650 lcd.drawFilledRectangle(COL2, 0, LCD_W, barHeight, GREY_DEFAULT)
651 lcd.drawGauge(0, 0, COL2, barHeight, fields_count - #loadQ, fields_count, 0)
652 else
653 lcd.drawFilledRectangle(0, 0, LCD_W, barHeight, GREY_DEFAULT)
654 if titleShowWarn then
655 lcd.drawText(textXoffset, 1, elrsFlagsInfo, INVERS)
656 else
657 local title = fields_count > 0 and deviceName or "Loading..."
658 lcd.drawText(textXoffset, 1, title, INVERS)
663 local function lcd_warn()
664 lcd.drawText(textXoffset, textSize*2, "Error:")
665 lcd.drawText(textXoffset, textSize*3, elrsFlagsInfo)
666 lcd.drawText(LCD_W/2, textSize*5, "[OK]", BLINK + INVERS + CENTER)
669 local function reloadCurField()
670 local field = getField(lineIndex)
671 fieldTimeout = 0
672 fieldChunk = 0
673 fieldData = {}
674 loadQ[#loadQ+1] = field.id
677 local function reloadRelatedFields(field)
678 -- Reload the parent folder to update the description
679 if field.parent then
680 loadQ[#loadQ+1] = field.parent
681 fields[field.parent].name = nil
684 -- Reload all editable fields at the same level as well as the parent item
685 for fieldId = fields_count, 1, -1 do
686 -- Skip this field, will be added to end
687 local fldTest = fields[fieldId]
688 if fieldId ~= field.id
689 and fldTest.parent == field.parent
690 and (fldTest.type or 99) < 11 then -- type could be nil if still loading
691 fldTest.nc = true -- "no cache" the options
692 loadQ[#loadQ+1] = fieldId
696 -- Reload this field
697 loadQ[#loadQ+1] = field.id
698 -- with a short delay to allow the module EEPROM to commit
699 fieldTimeout = getTime() + 20
702 local function handleDevicePageEvent(event)
703 if #fields == 0 then --if there is no field yet
704 return
705 else
706 if fields[backButtonId].name == nil then --if back button is not assigned yet, means there is no field yet.
707 return
711 if event == EVT_VIRTUAL_EXIT then -- Cancel edit / go up a folder / reload all
712 if edit then
713 edit = nil
714 reloadCurField()
715 else
716 if folderAccess == nil and #loadQ == 0 then -- only do reload if we're in the root folder and finished loading
717 if deviceId ~= 0xEE then
718 changeDeviceId(0xEE) --change device id clear the fields_count, therefore the next ping will do reloadAllField()
719 else
720 reloadAllField()
722 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
724 UIbackExec()
726 elseif event == EVT_VIRTUAL_ENTER then -- toggle editing/selecting current field
727 if elrsFlags > 0x1F then
728 elrsFlags = 0
729 crossfireTelemetryPush(0x2D, { deviceId, handsetId, 0x2E, 0x00 })
730 else
731 local field = getField(lineIndex)
732 if field and field.name then
733 if field.type < 10 then
734 edit = not edit
736 if not edit then
737 if field.type < 10 then
738 -- Editable fields
739 reloadRelatedFields(field)
740 elseif field.type == 13 then
741 -- Command
742 reloadCurField()
744 if functions[field.type+1].save then
745 functions[field.type+1].save(field)
750 elseif edit then
751 if event == EVT_VIRTUAL_NEXT then
752 incrField(1)
753 elseif event == EVT_VIRTUAL_PREV then
754 incrField(-1)
756 else
757 if event == EVT_VIRTUAL_NEXT then
758 selectField(1)
759 elseif event == EVT_VIRTUAL_PREV then
760 selectField(-1)
765 -- Main
766 local function runDevicePage(event)
767 handleDevicePageEvent(event)
769 lcd_title()
771 if #devices > 1 then -- show other device folder
772 fields[fields_count+1].parent = nil
774 if elrsFlags > 0x1F then
775 lcd_warn()
776 else
777 for y = 1, maxLineIndex+1 do
778 local field = getField(pageOffset+y)
779 if not field then
780 break
781 elseif field.name ~= nil then
782 local attr = lineIndex == (pageOffset+y)
783 and ((edit and BLINK or 0) + INVERS)
784 or 0
785 if field.type < 11 or field.type == 12 then -- if not folder, command, or back
786 lcd.drawText(textXoffset, y*textSize+textYoffset, field.name, 0)
788 if functions[field.type+1].display then
789 functions[field.type+1].display(field, y*textSize+textYoffset, attr)
796 local function popupCompat(t, m, e)
797 -- Only use 2 of 3 arguments for older platforms
798 return popupConfirmation(t, e)
801 local function runPopupPage(event)
802 if event == EVT_VIRTUAL_EXIT then
803 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 5 }) -- lcsCancel
804 fieldTimeout = getTime() + 200 -- 2s
807 local result
808 if fieldPopup.status == 0 and fieldPopup.lastStatus ~= 0 then -- stopped
809 popupCompat(fieldPopup.info, "Stopped!", event)
810 reloadAllField()
811 fieldPopup = nil
812 elseif fieldPopup.status == 3 then -- confirmation required
813 result = popupCompat(fieldPopup.info, "PRESS [OK] to confirm", event)
814 fieldPopup.lastStatus = fieldPopup.status
815 if result == "OK" then
816 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 4 }) -- lcsConfirmed
817 fieldTimeout = getTime() + fieldPopup.timeout -- we are expecting an immediate response
818 fieldPopup.status = 4
819 elseif result == "CANCEL" then
820 fieldPopup = nil
822 elseif fieldPopup.status == 2 then -- running
823 if fieldChunk == 0 then
824 commandRunningIndicator = (commandRunningIndicator % 4) + 1
826 result = popupCompat(fieldPopup.info .. " [" .. string.sub("|/-\\", commandRunningIndicator, commandRunningIndicator) .. "]", "Press [RTN] to exit", event)
827 fieldPopup.lastStatus = fieldPopup.status
828 if result == "CANCEL" then
829 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 5 }) -- lcsCancel
830 fieldTimeout = getTime() + fieldPopup.timeout -- we are expecting an immediate response
831 fieldPopup = nil
836 local function loadSymbolChars()
837 -- On firmwares that have constants defined for the arrow chars, use them in place of
838 -- the \xc0 \xc1 chars (which are OpenTX-en)
839 if __opentx then
840 byteToStr = function (b)
841 -- Use the table to convert the char, else use string.char if not in the table
842 return ({
843 [192] = __opentx.CHAR_UP,
844 [193] = __opentx.CHAR_DOWN
845 })[b] or string.char(b)
847 else
848 byteToStr = string.char
852 local function touch2evt(event, touchState)
853 -- Convert swipe events to normal events Left/Right/Up/Down -> EXIT/ENTER/PREV/NEXT
854 -- PREV/NEXT are swapped if editing
855 -- TAP is converted to ENTER
856 touchState = touchState or {}
857 return (touchState.swipeLeft and EVT_VIRTUAL_EXIT)
858 or (touchState.swipeRight and EVT_VIRTUAL_ENTER)
859 or (touchState.swipeUp and (edit and EVT_VIRTUAL_NEXT or EVT_VIRTUAL_PREV))
860 or (touchState.swipeDown and (edit and EVT_VIRTUAL_PREV or EVT_VIRTUAL_NEXT))
861 or (event == EVT_TOUCH_TAP and EVT_VIRTUAL_ENTER)
864 local function setLCDvar()
865 -- Set the title function depending on if LCD is color, and free the other function and
866 -- set textselection unit function, use GetLastPost or sizeText
867 if (lcd.RGB ~= nil) then
868 lcd_title = lcd_title_color
869 functions[10].display=fieldTextSelectionDisplay_color
870 else
871 lcd_title = lcd_title_bw
872 functions[10].display=fieldTextSelectionDisplay_bw
873 touch2evt = nil
875 lcd_title_color = nil
876 lcd_title_bw = nil
877 fieldTextSelectionDisplay_bw = nil
878 fieldTextSelectionDisplay_color = nil
879 -- Determine if popupConfirmation takes 3 arguments or 2
880 -- if pcall(popupConfirmation, "", "", EVT_VIRTUAL_EXIT) then
881 -- major 1 is assumed to be FreedomTX
882 local ver, radio, major = getVersion()
883 if major ~= 1 then
884 popupCompat = popupConfirmation
886 if LCD_W == 480 then
887 COL2 = 240
888 maxLineIndex = 10
889 textXoffset = 3
890 textYoffset = 10
891 textSize = 22 --textSize is text Height
892 elseif LCD_W == 320 then
893 COL2 = 160
894 maxLineIndex = 14
895 textXoffset = 3
896 textYoffset = 10
897 textSize = 22
898 else
899 if LCD_W == 212 then
900 COL2 = 110
901 else
902 COL2 = 70
904 maxLineIndex = 6
905 textXoffset = 0
906 textYoffset = 3
907 textSize = 8
909 loadSymbolChars()
910 loadSymbolChars = nil
913 local function setMock()
914 -- Setup fields to display if running in Simulator
915 local _, rv = getVersion()
916 if string.sub(rv, -5) ~= "-simu" then return end
917 local mock = loadScript("mockup/elrsmock.lua")
918 if mock == nil then return end
919 fields, goodBadPkt, deviceName = mock(), "0/500 C", "ExpressLRS TX"
920 fields_count = #fields - 1
921 loadQ = { fields_count }
922 deviceIsELRS_TX = true
923 backButtonId = #fields
925 fields_count = fields_count + 1
926 exitButtonId = fields_count + 1
927 fields[exitButtonId] = {id = exitButtonId, name="----EXIT----", type=17}
930 -- Init
931 local function init()
932 setLCDvar()
933 setMock()
934 setLCDvar = nil
935 setMock = nil
938 -- Main
939 local function run(event, touchState)
940 if event == nil then
941 error("Cannot be run as a model script!")
942 return 2
945 local forceRedraw = refreshNext()
947 event = (touch2evt and touch2evt(event, touchState)) or event
948 if fieldPopup ~= nil then
949 runPopupPage(event)
950 elseif event ~= 0 or forceRedraw or edit then
951 runDevicePage(event)
954 return exitscript
957 return { init=init, run=run }