Parse MSP messages from the backpack while TX is in mavlink mode (#2883)
[ExpressLRS.git] / src / lua / elrsV3.lua
blob3af4dbb34131840aa6d87517daacf87c3f0af5b2
1 -- TNS|ExpressLRS|TNE
2 ---- #########################################################################
3 ---- # #
4 ---- # Copyright (C) OpenTX, adapted for ExpressLRS #
5 -----# #
6 ---- # License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html #
7 ---- # #
8 ---- #########################################################################
9 local deviceId = 0xEE
10 local handsetId = 0xEF
11 local deviceName = ""
12 local lineIndex = 1
13 local pageOffset = 0
14 local edit = nil
15 local fieldPopup
16 local fieldTimeout = 0
17 local loadQ = {}
18 local fieldChunk = 0
19 local fieldData = nil
20 local fields = {}
21 local devices = {}
22 local goodBadPkt = "?/??? ?"
23 local elrsFlags = 0
24 local elrsFlagsInfo = ""
25 local fields_count = 0
26 local devicesRefreshTimeout = 50
27 local currentFolderId = nil
28 local commandRunningIndicator = 1
29 local expectChunksRemain = -1
30 local deviceIsELRS_TX = nil
31 local linkstatTimeout = 100
32 local titleShowWarn = nil
33 local titleShowWarnTimeout = 100
34 local exitscript = 0
36 local COL1
37 local COL2
38 local maxLineIndex
39 local textYoffset
40 local textSize
42 local function allocateFields()
43 fields = {}
44 for i=1, fields_count + 2 + #devices do
45 fields[i] = { }
46 end
47 fields[#fields] = {name="----EXIT----", type=14}
48 end
50 local function reloadAllField()
51 fieldChunk = 0
52 fieldData = nil
53 -- loadQ is actually a stack
54 loadQ = {}
55 for fieldId = fields_count, 1, -1 do
56 loadQ[#loadQ+1] = fieldId
57 end
58 end
60 local function getField(line)
61 local counter = 1
62 for i = 1, #fields do
63 local field = fields[i]
64 if currentFolderId == field.parent and not field.hidden then
65 if counter < line then
66 counter = counter + 1
67 else
68 return field
69 end
70 end
71 end
72 end
74 local function incrField(step)
75 local field = getField(lineIndex)
76 local min, max = 0, 0
77 if field.type <= 8 then
78 min = field.min or 0
79 max = field.max or 0
80 step = (field.step or 1) * step
81 elseif field.type == 9 then
82 min = 0
83 max = #field.values - 1
84 end
86 local newval = field.value
87 repeat
88 newval = newval + step
89 if newval < min then
90 newval = min
91 elseif newval > max then
92 newval = max
93 end
95 -- keep looping until a non-blank selection value is found
96 if field.values == nil or #field.values[newval+1] ~= 0 then
97 field.value = newval
98 return
99 end
100 until (newval == min or newval == max)
103 -- Select the next or previous editable field
104 local function selectField(step)
105 local newLineIndex = lineIndex
106 local field
107 repeat
108 newLineIndex = newLineIndex + step
109 if newLineIndex <= 0 then
110 newLineIndex = #fields
111 elseif newLineIndex == 1 + #fields then
112 newLineIndex = 1
113 pageOffset = 0
115 field = getField(newLineIndex)
116 until newLineIndex == lineIndex or (field and field.name)
117 lineIndex = newLineIndex
118 if lineIndex > maxLineIndex + pageOffset then
119 pageOffset = lineIndex - maxLineIndex
120 elseif lineIndex <= pageOffset then
121 pageOffset = lineIndex - 1
125 local function fieldGetStrOrOpts(data, offset, last, isOpts)
126 -- For isOpts: Split a table of byte values (string) with ; separator into a table
127 -- Else just read a string until the first null byte
128 local r = last or (isOpts and {})
129 local opt = ''
130 local vcnt = 0
131 repeat
132 local b = data[offset]
133 offset = offset + 1
135 if not last then
136 if r and (b == 59 or b == 0) then -- ';'
137 r[#r+1] = opt
138 if opt ~= '' then
139 vcnt = vcnt + 1
140 opt = ''
142 elseif b ~= 0 then
143 -- On firmwares that have constants defined for the arrow chars, use them in place of
144 -- the \xc0 \xc1 chars (which are OpenTX-en)
145 -- Use the table to convert the char, else use string.char if not in the table
146 opt = opt .. (({
147 [192] = CHAR_UP or (__opentx and __opentx.CHAR_UP),
148 [193] = CHAR_DOWN or (__opentx and __opentx.CHAR_DOWN)
149 })[b] or string.char(b))
152 until b == 0
154 return (r or opt), offset, vcnt, collectgarbage("collect")
157 local function getDevice(name)
158 for i=1, #devices do
159 if devices[i].name == name then
160 return devices[i]
165 local function fieldGetValue(data, offset, size)
166 local result = 0
167 for i=0, size-1 do
168 result = bit32.lshift(result, 8) + data[offset + i]
170 return result
173 local function reloadCurField()
174 local field = getField(lineIndex)
175 fieldTimeout = 0
176 fieldChunk = 0
177 fieldData = nil
178 loadQ[#loadQ+1] = field.id
181 -- UINT8/INT8/UINT16/INT16 + FLOAT + TEXTSELECT
182 local function fieldUnsignedLoad(field, data, offset, size, unitoffset)
183 field.value = fieldGetValue(data, offset, size)
184 field.min = fieldGetValue(data, offset+size, size)
185 field.max = fieldGetValue(data, offset+2*size, size)
186 --field.default = fieldGetValue(data, offset+3*size, size)
187 field.unit = fieldGetStrOrOpts(data, offset+(unitoffset or (4*size)), field.unit)
188 -- Only store the size if it isn't 1 (covers most fields / selection)
189 if size ~= 1 then
190 field.size = size
194 local function fieldUnsignedToSigned(field, size)
195 local bandval = bit32.lshift(0x80, (size-1)*8)
196 field.value = field.value - bit32.band(field.value, bandval) * 2
197 field.min = field.min - bit32.band(field.min, bandval) * 2
198 field.max = field.max - bit32.band(field.max, bandval) * 2
199 --field.default = field.default - bit32.band(field.default, bandval) * 2
202 local function fieldSignedLoad(field, data, offset, size, unitoffset)
203 fieldUnsignedLoad(field, data, offset, size, unitoffset)
204 fieldUnsignedToSigned(field, size)
205 -- signed ints are INTdicated by a negative size
206 field.size = -size
209 local function fieldIntLoad(field, data, offset)
210 -- Type is U8/I8/U16/I16, use that to determine the size and signedness
211 local loadFn = (field.type % 2 == 0) and fieldUnsignedLoad or fieldSignedLoad
212 loadFn(field, data, offset, math.floor(field.type / 2) + 1)
215 local function fieldIntSave(field)
216 local value = field.value
217 local size = field.size or 1
218 -- Convert signed to 2s complement
219 if size < 0 then
220 size = -size
221 if value < 0 then
222 value = bit32.lshift(0x100, (size-1)*8) + value
226 local frame = { deviceId, handsetId, field.id }
227 for i = size-1, 0, -1 do
228 frame[#frame + 1] = bit32.rshift(value, 8*i) % 256
230 crossfireTelemetryPush(0x2D, frame)
233 local function fieldIntDisplay(field, y, attr)
234 lcd.drawText(COL2, y, field.value .. field.unit, attr)
237 -- -- FLOAT
238 local function fieldFloatLoad(field, data, offset)
239 fieldSignedLoad(field, data, offset, 4, 21)
240 field.prec = data[offset+16]
241 if field.prec > 3 then
242 field.prec = 3
244 field.step = fieldGetValue(data, offset+17, 4)
246 -- precompute the format string to preserve the precision
247 field.fmt = "%." .. tostring(field.prec) .. "f" .. field.unit
248 -- Convert precision to a divider
249 field.prec = 10 ^ field.prec
252 local function fieldFloatDisplay(field, y, attr)
253 lcd.drawText(COL2, y, string.format(field.fmt, field.value / field.prec), attr)
256 -- TEXT SELECTION
257 local function fieldTextSelLoad(field, data, offset)
258 local vcnt
259 local cached = field.nc == nil and field.values
260 field.values, offset, vcnt = fieldGetStrOrOpts(data, offset, cached, true)
261 -- 'Disable' the line if values only has one option in the list
262 if not cached then
263 field.grey = vcnt <= 1
265 field.value = data[offset]
266 -- min max and default (offset+1 to 3) are not used on selections
267 -- units never uses cache
268 field.unit = fieldGetStrOrOpts(data, offset+4)
269 field.nc = nil -- use cache next time
272 local function fieldTextSelDisplay_color(field, y, attr, color)
273 local val = field.values[field.value+1] or "ERR"
274 lcd.drawText(COL2, y, val, attr + color)
275 local strPix = lcd.sizeText and lcd.sizeText(val) or (10 * #val)
276 lcd.drawText(COL2 + strPix, y, field.unit, color)
279 local function fieldTextSelDisplay_bw(field, y, attr)
280 lcd.drawText(COL2, y, field.values[field.value+1] or "ERR", attr)
281 lcd.drawText(lcd.getLastPos(), y, field.unit, 0)
284 -- STRING
285 local function fieldStringLoad(field, data, offset)
286 field.value, offset = fieldGetStrOrOpts(data, offset)
287 if #data >= offset then
288 field.maxlen = data[offset]
292 local function fieldStringDisplay(field, y, attr)
293 lcd.drawText(COL2, y, field.value, attr)
296 local function fieldFolderOpen(field)
297 currentFolderId = field.id
298 local backFld = fields[#fields]
299 backFld.name = "----BACK----"
300 -- Store the lineIndex and pageOffset to return to in the backFld
301 backFld.li = lineIndex
302 backFld.po = pageOffset
303 backFld.parent = currentFolderId
305 lineIndex = 1
306 pageOffset = 0
309 local function fieldFolderDeviceOpen(field)
310 crossfireTelemetryPush(0x28, { 0x00, 0xEA }) --broadcast with standard handset ID to get all node respond correctly
311 return fieldFolderOpen(field)
314 local function fieldFolderDisplay(field,y ,attr)
315 lcd.drawText(COL1, y, "> " .. field.name, attr + BOLD)
318 local function fieldCommandLoad(field, data, offset)
319 field.status = data[offset]
320 field.timeout = data[offset+1]
321 field.info = fieldGetStrOrOpts(data, offset+2)
322 if field.status == 0 then
323 fieldPopup = nil
327 local function fieldCommandSave(field)
328 reloadCurField()
330 if field.status ~= nil then
331 if field.status < 4 then
332 field.status = 1
333 crossfireTelemetryPush(0x2D, { deviceId, handsetId, field.id, field.status })
334 fieldPopup = field
335 fieldPopup.lastStatus = 0
336 fieldTimeout = getTime() + field.timeout
341 local function fieldCommandDisplay(field, y, attr)
342 lcd.drawText(10, y, "[" .. field.name .. "]", attr + BOLD)
345 local function fieldBackExec(field)
346 if field.parent then
347 lineIndex = field.li or 1
348 pageOffset = field.po or 0
350 field.name = "----EXIT----"
351 field.parent = nil
352 field.li = nil
353 field.po = nil
354 currentFolderId = nil
355 else
356 exitscript = 1
360 local function changeDeviceId(devId) --change to selected device ID
361 currentFolderId = nil
362 deviceIsELRS_TX = nil
363 elrsFlags = 0
364 --if the selected device ID (target) is a TX Module, we use our Lua ID, so TX Flag that user is using our LUA
365 if devId == 0xEE then
366 handsetId = 0xEF
367 else --else we would act like the legacy lua
368 handsetId = 0xEA
370 deviceId = devId
371 fields_count = 0 --set this because next target wouldn't have the same count, and this trigger to request the new count
374 local function fieldDeviceIdSelect(field)
375 local device = getDevice(field.name)
376 changeDeviceId(device.id)
377 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
380 local function createDeviceFields() -- put other devices in the field list
381 -- move back button to the end of the list, so it will always show up at the bottom.
382 fields[fields_count + 2 + #devices] = fields[#fields]
383 for i=1, #devices do
384 local parent = (devices[i].id == deviceId) and 255 or (fields_count+1)
385 fields[fields_count+1+i] = {name=devices[i].name, parent=parent, type=15}
389 local function parseDeviceInfoMessage(data)
390 local offset
391 local id = data[2]
392 local newName
393 newName, offset = fieldGetStrOrOpts(data, 3)
394 local device = getDevice(newName)
395 if device == nil then
396 device = { id = id, name = newName }
397 devices[#devices + 1] = device
399 if deviceId == id then
400 deviceName = newName
401 deviceIsELRS_TX = ((fieldGetValue(data,offset,4) == 0x454C5253) and (deviceId == 0xEE)) or nil -- SerialNumber = 'E L R S' and ID is TX module
402 local newFieldCount = data[offset+12]
403 if newFieldCount ~= fields_count or newFieldCount == 0 then
404 fields_count = newFieldCount
405 allocateFields()
406 reloadAllField()
407 fields[fields_count+1] = {id = fields_count+1, name="Other Devices", parent = 255, type=16} -- add other devices folders
408 if newFieldCount == 0 then
409 -- This device has no fields so the Loading code never starts
410 createDeviceFields()
416 local functions = {
417 { load=fieldIntLoad, save=fieldIntSave, display=fieldIntDisplay }, --1 UINT8(0)
418 { load=fieldIntLoad, save=fieldIntSave, display=fieldIntDisplay }, --2 INT8(1)
419 { load=fieldIntLoad, save=fieldIntSave, display=fieldIntDisplay }, --3 UINT16(2)
420 { load=fieldIntLoad, save=fieldIntSave, display=fieldIntDisplay }, --4 INT16(3)
421 nil,
422 nil,
423 nil,
424 nil,
425 { load=fieldFloatLoad, save=fieldIntSave, display=fieldFloatDisplay }, --9 FLOAT(8)
426 { load=fieldTextSelLoad, save=fieldIntSave, display=nil }, --10 SELECT(9)
427 { load=fieldStringLoad, save=nil, display=fieldStringDisplay }, --11 STRING(10) editing NOTIMPL
428 { load=nil, save=fieldFolderOpen, display=fieldFolderDisplay }, --12 FOLDER(11)
429 { load=fieldStringLoad, save=nil, display=fieldStringDisplay }, --13 INFO(12)
430 { load=fieldCommandLoad, save=fieldCommandSave, display=fieldCommandDisplay }, --14 COMMAND(13)
431 { load=nil, save=fieldBackExec, display=fieldCommandDisplay }, --15 back/exit(14)
432 { load=nil, save=fieldDeviceIdSelect, display=fieldCommandDisplay }, --16 device(15)
433 { load=nil, save=fieldFolderDeviceOpen, display=fieldFolderDisplay }, --17 deviceFOLDER(16)
436 local function parseParameterInfoMessage(data)
437 local fieldId = (fieldPopup and fieldPopup.id) or loadQ[#loadQ]
438 if data[2] ~= deviceId or data[3] ~= fieldId then
439 fieldData = nil
440 fieldChunk = 0
441 return
443 local field = fields[fieldId]
444 local chunksRemain = data[4]
445 -- If no field or the chunksremain changed when we have data, don't continue
446 if not field or (fieldData and chunksRemain ~= expectChunksRemain) then
447 return
450 local offset
451 -- If data is chunked, copy it to persistent buffer
452 if chunksRemain > 0 or fieldChunk > 0 then
453 fieldData = fieldData or {}
454 for i=5, #data do
455 fieldData[#fieldData + 1] = data[i]
456 data[i] = nil
458 offset = 1
459 else
460 -- All data arrived in one chunk, operate directly on data
461 fieldData = data
462 offset = 5
465 if chunksRemain > 0 then
466 fieldChunk = fieldChunk + 1
467 expectChunksRemain = chunksRemain - 1
468 else
469 -- Field data stream is now complete, process into a field
470 loadQ[#loadQ] = nil
472 if #fieldData > (offset + 2) then
473 field.id = fieldId
474 field.parent = (fieldData[offset] ~= 0) and fieldData[offset] or nil
475 field.type = bit32.band(fieldData[offset+1], 0x7f)
476 field.hidden = bit32.btest(fieldData[offset+1], 0x80) or nil
477 field.name, offset = fieldGetStrOrOpts(fieldData, offset+2, field.name)
478 if functions[field.type+1].load then
479 functions[field.type+1].load(field, fieldData, offset)
481 if field.min == 0 then field.min = nil end
482 if field.max == 0 then field.max = nil end
485 fieldChunk = 0
486 fieldData = nil
488 -- Last field loaded, add the list of devices to the end
489 if #loadQ == 0 then
490 createDeviceFields()
493 -- Return value is if the screen should be updated
494 -- If deviceId is TX module, then the Bad/Good drives the update; for other
495 -- devices update each new item. and always update when the queue empties
496 return deviceId ~= 0xEE or #loadQ == 0
500 local function parseElrsInfoMessage(data)
501 if data[2] ~= deviceId then
502 fieldData = nil
503 fieldChunk = 0
504 return
507 local badPkt = data[3]
508 local goodPkt = (data[4]*256) + data[5]
509 local newFlags = data[6]
510 -- If flags are changing, reset the warning timeout to display/hide message immediately
511 if newFlags ~= elrsFlags then
512 elrsFlags = newFlags
513 titleShowWarnTimeout = 0
515 elrsFlagsInfo = fieldGetStrOrOpts(data, 7)
517 local state = (bit32.btest(elrsFlags, 1) and "C") or "-"
518 goodBadPkt = string.format("%u/%u %s", badPkt, goodPkt, state)
521 local function parseElrsV1Message(data)
522 if (data[1] ~= 0xEA) or (data[2] ~= 0xEE) then
523 return
526 -- local badPkt = data[9]
527 -- local goodPkt = (data[10]*256) + data[11]
528 -- goodBadPkt = string.format("%u/%u X", badPkt, goodPkt)
529 fieldPopup = {id = 0, status = 2, timeout = 0xFF, info = "ERROR: 1.x firmware"}
530 fieldTimeout = getTime() + 0xFFFF
533 local function refreshNext()
534 local command, data, forceRedraw
535 repeat
536 command, data = crossfireTelemetryPop()
537 if command == 0x29 then
538 parseDeviceInfoMessage(data)
539 elseif command == 0x2B then
540 if parseParameterInfoMessage(data) then
541 forceRedraw = true
543 if #loadQ > 0 then
544 fieldTimeout = 0 -- request next chunk immediately
545 elseif fieldPopup then
546 fieldTimeout = getTime() + fieldPopup.timeout
548 elseif command == 0x2D then
549 parseElrsV1Message(data)
550 elseif command == 0x2E then
551 parseElrsInfoMessage(data)
552 forceRedraw = true
554 until command == nil
556 local time = getTime()
557 if fieldPopup then
558 if time > fieldTimeout and fieldPopup.status ~= 3 then
559 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 6 }) -- lcsQuery
560 fieldTimeout = time + fieldPopup.timeout
562 elseif time > devicesRefreshTimeout and fields_count < 1 then
563 forceRedraw = true -- handles initial screen draw
564 devicesRefreshTimeout = time + 100 -- 1s
565 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
566 elseif time > linkstatTimeout then
567 if not deviceIsELRS_TX and #loadQ == 0 then
568 goodBadPkt = ""
569 else
570 crossfireTelemetryPush(0x2D, { deviceId, handsetId, 0x0, 0x0 }) --request linkstat
572 linkstatTimeout = time + 100
573 elseif time > fieldTimeout and fields_count ~= 0 then
574 if #loadQ > 0 then
575 crossfireTelemetryPush(0x2C, { deviceId, handsetId, loadQ[#loadQ], fieldChunk })
576 fieldTimeout = time + 50 -- 0.5s
580 if time > titleShowWarnTimeout then
581 -- if elrsFlags bit set is bit higher than bit 0 and bit 1, it is warning flags
582 titleShowWarn = (elrsFlags > 3 and not titleShowWarn) or nil
583 titleShowWarnTimeout = time + 100
584 forceRedraw = true
587 return forceRedraw
590 local lcd_title -- holds function that is color/bw version
591 local function lcd_title_color()
592 lcd.clear()
594 local EBLUE = lcd.RGB(0x43, 0x61, 0xAA)
595 local EGREEN = lcd.RGB(0x9f, 0xc7, 0x6f)
596 local EGREY1 = lcd.RGB(0x91, 0xb2, 0xc9)
597 local EGREY2 = lcd.RGB(0x6f, 0x62, 0x7f)
598 local barHeight = 30
600 -- Field display area (white w/ 2px green border)
601 lcd.setColor(CUSTOM_COLOR, EGREEN)
602 lcd.drawRectangle(0, 0, LCD_W, LCD_H, CUSTOM_COLOR)
603 lcd.drawRectangle(1, 0, LCD_W - 2, LCD_H - 1, CUSTOM_COLOR)
604 -- title bar
605 lcd.drawFilledRectangle(0, 0, LCD_W, barHeight, CUSTOM_COLOR)
606 lcd.setColor(CUSTOM_COLOR, EGREY1)
607 lcd.drawFilledRectangle(LCD_W - textSize, 0, textSize, barHeight, CUSTOM_COLOR)
608 lcd.setColor(CUSTOM_COLOR, EGREY2)
609 lcd.drawRectangle(LCD_W - textSize, 0, textSize, barHeight - 1, CUSTOM_COLOR)
610 lcd.drawRectangle(LCD_W - textSize, 1 , textSize - 1, barHeight - 2, CUSTOM_COLOR) -- left and bottom line only 1px, make it look bevelled
611 lcd.setColor(CUSTOM_COLOR, BLACK)
612 if titleShowWarn then
613 lcd.drawText(COL1 + 1, 4, elrsFlagsInfo, CUSTOM_COLOR)
614 else
615 local title = fields_count > 0 and deviceName or "Loading..."
616 lcd.drawText(COL1 + 1, 4, title, CUSTOM_COLOR)
617 lcd.drawText(LCD_W - 5, 4, goodBadPkt, RIGHT + BOLD + CUSTOM_COLOR)
619 -- progress bar
620 if #loadQ > 0 and fields_count > 0 then
621 local barW = (COL2-4) * (fields_count - #loadQ) / fields_count
622 lcd.setColor(CUSTOM_COLOR, EBLUE)
623 lcd.drawFilledRectangle(2, 2+20, barW, barHeight-5-20, CUSTOM_COLOR)
624 lcd.setColor(CUSTOM_COLOR, WHITE)
625 lcd.drawFilledRectangle(2+barW, 2+20, COL2-2-barW, barHeight-5-20, CUSTOM_COLOR)
629 local function lcd_title_bw()
630 lcd.clear()
631 -- B&W screen
632 local barHeight = 9
633 if not titleShowWarn then
634 lcd.drawText(LCD_W - 1, 1, goodBadPkt, RIGHT)
635 lcd.drawLine(LCD_W - 10, 0, LCD_W - 10, barHeight-1, SOLID, INVERS)
638 if #loadQ > 0 and fields_count > 0 then
639 lcd.drawFilledRectangle(COL2, 0, LCD_W, barHeight, GREY_DEFAULT)
640 lcd.drawGauge(0, 0, COL2, barHeight, fields_count - #loadQ, fields_count, 0)
641 else
642 lcd.drawFilledRectangle(0, 0, LCD_W, barHeight, GREY_DEFAULT)
643 if titleShowWarn then
644 lcd.drawText(COL1, 1, elrsFlagsInfo, INVERS)
645 else
646 local title = fields_count > 0 and deviceName or "Loading..."
647 lcd.drawText(COL1, 1, title, INVERS)
652 local function lcd_warn()
653 lcd.drawText(COL1, textSize*2, "Error:")
654 lcd.drawText(COL1, textSize*3, elrsFlagsInfo)
655 lcd.drawText(LCD_W/2, textSize*5, "[OK]", BLINK + INVERS + CENTER)
658 local function reloadRelatedFields(field)
659 -- Reload the parent folder to update the description
660 if field.parent then
661 loadQ[#loadQ+1] = field.parent
662 fields[field.parent].name = nil
665 -- Reload all editable fields at the same level as well as the parent item
666 for fieldId = fields_count, 1, -1 do
667 -- Skip this field, will be added to end
668 local fldTest = fields[fieldId]
669 local fldType = fldTest.type or 99 -- type could be nil if still loading
670 if fieldId ~= field.id
671 and fldTest.parent == field.parent
672 and (fldType < 11 or fldType == 12) then -- ignores FOLDER/COMMAND/devices/EXIT
673 fldTest.nc = true -- "no cache" the options
674 loadQ[#loadQ+1] = fieldId
678 -- Reload this field
679 loadQ[#loadQ+1] = field.id
680 -- with a short delay to allow the module EEPROM to commit
681 fieldTimeout = getTime() + 20
684 local function handleDevicePageEvent(event)
685 if #fields == 0 then --if there is no field yet
686 return
687 else
688 if fields[#fields].name == nil then --if back button is not assigned yet, means there is no field yet.
689 return
693 if event == EVT_VIRTUAL_EXIT then -- Cancel edit / go up a folder / reload all
694 if edit then
695 edit = nil
696 reloadCurField()
697 else
698 if currentFolderId == nil and #loadQ == 0 then -- only do reload if we're in the root folder and finished loading
699 if deviceId ~= 0xEE then
700 changeDeviceId(0xEE) --change device id clear the fields_count, therefore the next ping will do reloadAllField()
701 else
702 reloadAllField()
704 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
705 else
706 fieldBackExec(fields[#fields])
709 elseif event == EVT_VIRTUAL_ENTER then -- toggle editing/selecting current field
710 if elrsFlags > 0x1F then
711 elrsFlags = 0
712 crossfireTelemetryPush(0x2D, { deviceId, handsetId, 0x2E, 0x00 })
713 else
714 local field = getField(lineIndex)
715 if field and field.name then
716 -- Editable fields
717 if not field.grey and field.type < 10 then
718 edit = not edit
719 if not edit then
720 reloadRelatedFields(field)
723 if not edit then
724 if functions[field.type+1].save then
725 functions[field.type+1].save(field)
730 elseif edit then
731 if event == EVT_VIRTUAL_NEXT then
732 incrField(1)
733 elseif event == EVT_VIRTUAL_PREV then
734 incrField(-1)
736 else
737 if event == EVT_VIRTUAL_NEXT then
738 selectField(1)
739 elseif event == EVT_VIRTUAL_PREV then
740 selectField(-1)
745 -- Main
746 local function runDevicePage(event)
747 handleDevicePageEvent(event)
749 lcd_title()
751 if #devices > 1 then -- show other device folder
752 fields[fields_count+1].parent = nil
754 if elrsFlags > 0x1F then
755 lcd_warn()
756 else
757 for y = 1, maxLineIndex+1 do
758 local field = getField(pageOffset+y)
759 if not field then
760 break
761 elseif field.name ~= nil then
762 local attr = lineIndex == (pageOffset+y)
763 and ((edit and BLINK or 0) + INVERS)
764 or 0
765 local color = field.grey and COLOR_THEME_DISABLED or 0
766 if field.type < 11 or field.type == 12 then -- if not folder, command, or back
767 lcd.drawText(COL1, y*textSize+textYoffset, field.name, color)
769 if functions[field.type+1].display then
770 functions[field.type+1].display(field, y*textSize+textYoffset, attr, color)
777 local function popupCompat(t, m, e)
778 -- Only use 2 of 3 arguments for older platforms
779 return popupConfirmation(t, e)
782 local function runPopupPage(event)
783 if event == EVT_VIRTUAL_EXIT then
784 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 5 }) -- lcsCancel
785 fieldTimeout = getTime() + 200 -- 2s
788 if fieldPopup.status == 0 and fieldPopup.lastStatus ~= 0 then -- stopped
789 popupCompat(fieldPopup.info, "Stopped!", event)
790 reloadAllField()
791 fieldPopup = nil
792 elseif fieldPopup.status == 3 then -- confirmation required
793 local result = popupCompat(fieldPopup.info, "PRESS [OK] to confirm", event)
794 fieldPopup.lastStatus = fieldPopup.status
795 if result == "OK" then
796 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 4 }) -- lcsConfirmed
797 fieldTimeout = getTime() + fieldPopup.timeout -- we are expecting an immediate response
798 fieldPopup.status = 4
799 elseif result == "CANCEL" then
800 fieldPopup = nil
802 elseif fieldPopup.status == 2 then -- running
803 if fieldChunk == 0 then
804 commandRunningIndicator = (commandRunningIndicator % 4) + 1
806 local result = popupCompat(fieldPopup.info .. " [" .. string.sub("|/-\\", commandRunningIndicator, commandRunningIndicator) .. "]", "Press [RTN] to exit", event)
807 fieldPopup.lastStatus = fieldPopup.status
808 if result == "CANCEL" then
809 crossfireTelemetryPush(0x2D, { deviceId, handsetId, fieldPopup.id, 5 }) -- lcsCancel
810 fieldTimeout = getTime() + fieldPopup.timeout -- we are expecting an immediate response
811 fieldPopup = nil
816 local function touch2evt(event, touchState)
817 -- Convert swipe events to normal events Left/Right/Up/Down -> EXIT/ENTER/PREV/NEXT
818 -- PREV/NEXT are swapped if editing
819 -- TAP is converted to ENTER
820 touchState = touchState or {}
821 return (touchState.swipeLeft and EVT_VIRTUAL_EXIT)
822 or (touchState.swipeRight and EVT_VIRTUAL_ENTER)
823 or (touchState.swipeUp and (edit and EVT_VIRTUAL_NEXT or EVT_VIRTUAL_PREV))
824 or (touchState.swipeDown and (edit and EVT_VIRTUAL_PREV or EVT_VIRTUAL_NEXT))
825 or (event == EVT_TOUCH_TAP and EVT_VIRTUAL_ENTER)
828 local function setLCDvar()
829 -- Set the title function depending on if LCD is color, and free the other function and
830 -- set textselection unit function, use GetLastPost or sizeText
831 if (lcd.RGB ~= nil) then
832 lcd_title = lcd_title_color
833 functions[10].display = fieldTextSelDisplay_color
834 else
835 lcd_title = lcd_title_bw
836 functions[10].display = fieldTextSelDisplay_bw
837 touch2evt = nil
839 lcd_title_color = nil
840 lcd_title_bw = nil
841 fieldTextSelDisplay_bw = nil
842 fieldTextSelDisplay_color = nil
843 -- Determine if popupConfirmation takes 3 arguments or 2
844 -- if pcall(popupConfirmation, "", "", EVT_VIRTUAL_EXIT) then
845 -- major 1 is assumed to be FreedomTX
846 local _, _, major = getVersion()
847 if major ~= 1 then
848 popupCompat = popupConfirmation
850 if LCD_W == 480 then
851 COL1 = 3
852 COL2 = 240
853 if LCD_H == 320 then
854 maxLineIndex = 12
855 else
856 maxLineIndex = 10
858 textYoffset = 10
859 textSize = 22 --textSize is text Height
860 elseif LCD_W == 320 then
861 COL1 = 3
862 COL2 = 160
863 maxLineIndex = 14
864 textYoffset = 10
865 textSize = 22
866 else
867 if LCD_W == 212 then
868 COL2 = 110
869 else
870 COL2 = 70
872 if LCD_H == 96 then
873 maxLineIndex = 9
874 else
875 maxLineIndex = 6
877 COL1 = 0
878 textYoffset = 3
879 textSize = 8
883 local function setMock()
884 -- Setup fields to display if running in Simulator
885 local _, rv = getVersion()
886 if string.sub(rv, -5) ~= "-simu" then return end
887 local mock = loadScript("mockup/elrsmock.lua")
888 if mock == nil then return end
889 fields, goodBadPkt, deviceName = mock()
890 fields_count = #fields - 1
891 loadQ = { fields_count }
892 deviceIsELRS_TX = true
895 local function checkCrsfModule()
896 -- Loop through the modules and look for one set to CRSF (5)
897 for modIdx = 0, 1 do
898 local mod = model.getModule(modIdx)
899 if mod and mod.Type == 5 then
900 -- CRSF found
901 checkCrsfModule = nil
902 return 0
906 -- No CRSF module found, save an error message for run()
907 lcd.clear()
908 local y = 0
909 lcd.drawText(2, y, " No ExpressLRS", MIDSIZE)
910 y = y + (textSize * 2) - 2
911 local msgs = {
912 " Enable a CRSF Internal",
913 " or External module in",
914 " Model settings",
915 " If module is internal",
916 " also set Internal RF to",
917 " CRSF in SYS->Hardware",
919 for i, msg in ipairs(msgs) do
920 lcd.drawText(2, y, msg)
921 y = y + textSize
922 if i == 3 then
923 lcd.drawLine(0, y, LCD_W, y, SOLID, INVERS)
924 y = y + 2
928 return 0
931 -- Init
932 local function init()
933 setLCDvar()
934 setMock()
935 setLCDvar = nil
936 setMock = nil
939 -- Main
940 local function run(event, touchState)
941 if event == nil then return 2 end
942 if checkCrsfModule then return checkCrsfModule() end
944 local forceRedraw = refreshNext()
946 event = (touch2evt and touch2evt(event, touchState)) or event
947 if fieldPopup ~= nil then
948 runPopupPage(event)
949 elseif event ~= 0 or forceRedraw or edit then
950 runDevicePage(event)
953 return exitscript
956 return { init=init, run=run }