2 ---- #########################################################################
4 ---- # Copyright (C) OpenTX, adapted for ExpressLRS #
6 ---- # License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html #
8 ---- #########################################################################
10 local handsetId
= 0xEF
16 local fieldTimeout
= 0
22 local goodBadPkt
= "?/??? ?"
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
42 local function allocateFields()
44 for i
=1, fields_count
+ 2 + #devices
do
47 fields
[#fields
] = {name
="----EXIT----", type=14}
50 local function reloadAllField()
53 -- loadQ is actually a stack
55 for fieldId
= fields_count
, 1, -1 do
56 loadQ
[#loadQ
+1] = fieldId
60 local function getField(line
)
63 local field
= fields
[i
]
64 if currentFolderId
== field
.parent
and not field
.hidden
then
65 if counter
< line
then
74 local function incrField(step
)
75 local field
= getField(lineIndex
)
77 if field
.type <= 8 then
80 step
= (field
.step
or 1) * step
81 elseif field
.type == 9 then
83 max = #field
.values
- 1
86 local newval
= field
.value
88 newval
= newval
+ step
91 elseif newval
> max then
95 -- keep looping until a non-blank selection value is found
96 if field
.values
== nil or #field
.values
[newval
+1] ~= 0 then
100 until (newval
== min or newval
== max)
103 -- Select the next or previous editable field
104 local function selectField(step
)
105 local newLineIndex
= lineIndex
108 newLineIndex
= newLineIndex
+ step
109 if newLineIndex
<= 0 then
110 newLineIndex
= #fields
111 elseif newLineIndex
== 1 + #fields
then
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 {})
132 local b
= data
[offset
]
136 if r
and (b
== 59 or 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
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
))
154 return (r
or opt
), offset
, vcnt
, collectgarbage("collect")
157 local function getDevice(name
)
159 if devices
[i
].name
== name
then
165 local function fieldGetValue(data
, offset
, size
)
168 result
= bit32
.lshift(result
, 8) + data
[offset
+ i
]
173 local function reloadCurField()
174 local field
= getField(lineIndex
)
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)
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
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
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
)
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
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
)
257 local function fieldTextSelLoad(field
, data
, offset
)
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
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)
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
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
327 local function fieldCommandSave(field
)
330 if field
.status
~= nil then
331 if field
.status
< 4 then
333 crossfireTelemetryPush(0x2D, { deviceId
, handsetId
, field
.id
, field
.status
})
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
)
347 lineIndex
= field
.li
or 1
348 pageOffset
= field
.po
or 0
350 field
.name
= "----EXIT----"
354 currentFolderId
= nil
360 local function changeDeviceId(devId
) --change to selected device ID
361 currentFolderId
= nil
362 deviceIsELRS_TX
= nil
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
367 else --else we would act like the legacy lua
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
]
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
)
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
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
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
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)
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
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
451 -- If data is chunked, copy it to persistent buffer
452 if chunksRemain
> 0 or fieldChunk
> 0 then
453 fieldData
= fieldData
or {}
455 fieldData
[#fieldData
+ 1] = data
[i
]
460 -- All data arrived in one chunk, operate directly on data
465 if chunksRemain
> 0 then
466 fieldChunk
= fieldChunk
+ 1
467 expectChunksRemain
= chunksRemain
- 1
469 -- Field data stream is now complete, process into a field
472 if #fieldData
> (offset
+ 2) then
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
488 -- Last field loaded, add the list of devices to the end
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
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
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
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
536 command
, data
= crossfireTelemetryPop()
537 if command
== 0x29 then
538 parseDeviceInfoMessage(data
)
539 elseif command
== 0x2B then
540 if parseParameterInfoMessage(data
) 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
)
556 local time
= getTime()
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
570 crossfireTelemetryPush(0x2D, { deviceId
, handsetId
, 0x0, 0x0 }) --request linkstat
572 linkstatTimeout
= time
+ 100
573 elseif time
> fieldTimeout
and fields_count
~= 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
590 local lcd_title
-- holds function that is color/bw version
591 local function lcd_title_color()
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)
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
)
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
)
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
)
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()
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)
642 lcd
.drawFilledRectangle(0, 0, LCD_W
, barHeight
, GREY_DEFAULT
)
643 if titleShowWarn
then
644 lcd
.drawText(COL1
, 1, elrsFlagsInfo
, INVERS
)
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
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
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
688 if fields
[#fields
].name
== nil then --if back button is not assigned yet, means there is no field yet.
693 if event
== EVT_VIRTUAL_EXIT
then -- Cancel edit / go up a folder / reload all
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()
704 crossfireTelemetryPush(0x28, { 0x00, 0xEA })
706 fieldBackExec(fields
[#fields
])
709 elseif event
== EVT_VIRTUAL_ENTER
then -- toggle editing/selecting current field
710 if elrsFlags
> 0x1F then
712 crossfireTelemetryPush(0x2D, { deviceId
, handsetId
, 0x2E, 0x00 })
714 local field
= getField(lineIndex
)
715 if field
and field
.name
then
717 if not field
.grey
and field
.type < 10 then
720 reloadRelatedFields(field
)
724 if functions
[field
.type+1].save
then
725 functions
[field
.type+1].save(field
)
731 if event
== EVT_VIRTUAL_NEXT
then
733 elseif event
== EVT_VIRTUAL_PREV
then
737 if event
== EVT_VIRTUAL_NEXT
then
739 elseif event
== EVT_VIRTUAL_PREV
then
746 local function runDevicePage(event
)
747 handleDevicePageEvent(event
)
751 if #devices
> 1 then -- show other device folder
752 fields
[fields_count
+1].parent
= nil
754 if elrsFlags
> 0x1F then
757 for y
= 1, maxLineIndex
+1 do
758 local field
= getField(pageOffset
+y
)
761 elseif field
.name
~= nil then
762 local attr
= lineIndex
== (pageOffset
+y
)
763 and ((edit
and BLINK
or 0) + INVERS
)
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
)
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
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
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
835 lcd_title
= lcd_title_bw
836 functions
[10].display
= fieldTextSelDisplay_bw
839 lcd_title_color
= 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()
848 popupCompat
= popupConfirmation
859 textSize
= 22 --textSize is text Height
860 elseif LCD_W
== 320 then
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)
898 local mod = model
.getModule(modIdx
)
899 if mod and mod.Type
== 5 then
901 checkCrsfModule
= nil
906 -- No CRSF module found, save an error message for run()
909 lcd
.drawText(2, y
, " No ExpressLRS", MIDSIZE
)
910 y
= y
+ (textSize
* 2) - 2
912 " Enable a CRSF Internal",
913 " or External module in",
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
)
923 lcd
.drawLine(0, y
, LCD_W
, y
, SOLID
, INVERS
)
932 local function init()
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
949 elseif event
~= 0 or forceRedraw
or edit
then
956 return { init
=init
, run
=run
}