├── .gitignore ├── README.md ├── SCRIPTS └── simulator.lua └── images ├── scr1.png ├── scr2.png └── scr3.png /.gitignore: -------------------------------------------------------------------------------- 1 | Local 2 | simulator.luac 3 | simulator.txt 4 | opentx.sdcard.version 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua FPV Simulator 2 | 3 | ![](https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/master/images/scr1.png) 4 | ![](https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/master/images/scr2.png) 5 | ![](https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/master/images/scr3.png) 6 | 7 | The fisrt FPV drone simulator running directly on OpenTX transmitters! 8 | 9 | April Fools' joke which is not actually a joke 😉 10 | 11 | https://youtu.be/shNwYKozE4o 12 | 13 | #### Requirements: 14 | * Any OpenTX / EdgeTX radio 15 | * SD card 16 | 17 | #### How to play: 18 | * Control your drone with throttle, pitch and roll sticks, just as you control your real drone. 19 | * Get as many points as you can in 30 seconds. 20 | 21 | #### How to install: 22 | * Copy [`SCRIPTS/simulator.lua`](https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/master/SCRIPTS/simulator.lua) file from this repository to `SCRIPTS` directory of your SD card. 23 | * Long press `Menu` button to enter Radio Setup. Navigate to SD-card page (2/9). Find the simulator file. Long press `Enter` button and choose `Execute`. 24 | 25 | If sim looks blurry at high speed, change the value in line #7: `local lowFps = false` from `false` to `true`. 26 | Most transmitters LCDs have a very slow response time. They are not intended to display dynamic scenes with high FPS. 27 | -------------------------------------------------------------------------------- /SCRIPTS/simulator.lua: -------------------------------------------------------------------------------- 1 | -- FPV drone simulator for OpenTX 2 | -- Author: Alexey Stankevich @AlexeyStn 3 | 4 | local drone = {x = 0, y = 0, z = 0} 5 | local speed = {x = 0, y = 0, z = 0} 6 | 7 | local lowFps = false 8 | local fpsCounter = 0 9 | 10 | local gate = {w = 30, h = 30} 11 | local flag = {w = 6, h = 30} 12 | local track = {w = 50, h = 80} 13 | 14 | local rollScale = 30 15 | local pitchScale = 5 16 | local throttleScale = 40 17 | 18 | local minSpeed = 3 19 | 20 | local objectsN = 2 21 | local objects = {} 22 | local zObjectsStep = 1500 23 | 24 | local zScale = 300 25 | 26 | local raceTime = 30 27 | local startTime 28 | local finishTime 29 | local countDown 30 | 31 | local raceStarted = false 32 | local startTonePlayed = false 33 | local counter = nil 34 | 35 | local objectCounter = 0 36 | local bestResultPath = "/SCRIPTS/simulator.txt" 37 | local isNewBest = false 38 | 39 | local highRes = 0 40 | 41 | local function loadBestResult() 42 | local f = io.open(bestResultPath, "r") 43 | if f == nil then 44 | return nil 45 | end 46 | result = tonumber(io.read(f, 3)) 47 | return result 48 | end 49 | 50 | local function saveBestResult(result) 51 | local f = io.open(bestResultPath, "w") 52 | io.write(f, string.format("%3d", result)) 53 | io.close(f) 54 | end 55 | 56 | local function drawBorder(x1, y1, x2, y2) -- 1 far, 2 close 57 | if x1 == x2 then -- vertical 58 | if y2 >= LCD_H then y2 = LCD_H - 1 end 59 | else -- diagonal 60 | a = (y2 - y1) / (x2 - x1) 61 | b = (y1 * x2 - y2 * x1) / (x2 - x1) 62 | x0 = 0 63 | y0 = x0 * a + b 64 | if a < 0 and y0 < LCD_H and y0 >= (LCD_H/2 + 1) then -- left side 65 | x2 = x0 66 | y2 = y0 67 | else 68 | x0 = (LCD_W - 1) 69 | y0 = x0 * a + b 70 | if a > 0 and y0 < LCD_H and y0 >= (LCD_H/2 + 1) then -- right side 71 | x2 = x0 72 | y2 = y0 73 | else -- bottom side 74 | p = (LCD_H - 1 - y1) / (y2 - y1) 75 | y2 = LCD_H - 1 76 | x2 = x1 + (x2 - x1) * p 77 | if x2 < 0 then x2 = 0 end 78 | if x2 >= LCD_W then x2 = LCD_W - 1 end 79 | end 80 | end 81 | end 82 | lcd.drawLine(x1, y1, x2, y2, DOTTED, FORCE) 83 | end 84 | 85 | local function drawLandscape() 86 | z = zObjectsStep / 5 87 | w = track.w * 2 88 | yDispFar = LCD_H / 2 + 1 89 | yDispClose = (- drone.y * zScale) / z + LCD_H / 2 - 1 90 | xDispFar = LCD_W / 2 + 1 91 | xDispClose = ((w - drone.x) * zScale) / z + LCD_W / 2 92 | drawBorder(xDispFar, yDispFar, xDispClose, yDispClose) 93 | xDispFar = LCD_W / 2 - 1 94 | xDispClose = ((- w - drone.x) * zScale) / z + LCD_W / 2 95 | drawBorder(xDispFar, yDispFar, xDispClose, yDispClose) 96 | lcd.drawLine(0, LCD_H/2 + 1, LCD_W - 1, LCD_H/2 + 1, DOTTED, FORCE) -- horizon 97 | end 98 | 99 | local function drawLine(x1, y1, x2, y2, flag) 100 | if flag == 'h' then 101 | if y1 < 0 or y1 > LCD_H then return 0 end 102 | if x1 < 0 and x2 < 0 then return 0 end 103 | if x1 >= LCD_W and x2 >= LCD_W then return 0 end 104 | if x1 < 0 then x1 = 0 end 105 | if x2 < 0 then x2 = 0 end 106 | if x1 >= LCD_W then x1 = LCD_W - 1 end 107 | if x2 >= LCD_W then x2 = LCD_W - 1 end 108 | lcd.drawLine(x1, y1, x2, y2, SOLID, FORCE) 109 | return 0 110 | end 111 | if flag == 'v' then 112 | if x1 < 0 or x1 > LCD_W then return 0 end 113 | if y1 < 0 and y2 < 0 then return 0 end 114 | if y1 >= LCD_H and y2 >= LCD_H then return 0 end 115 | if y1 < 0 then y1 = 0 end 116 | if y2 < 0 then y2 = 0 end 117 | if y1 >= LCD_H then y1 = LCD_H - 1 end 118 | if y2 >= LCD_H then y2 = LCD_H - 1 end 119 | lcd.drawLine(x1, y1, x2, y2, SOLID, FORCE) 120 | return 0 121 | end 122 | end 123 | 124 | local function drawMarker(x, y) 125 | if x < 0 then x = 1 end 126 | if x >= LCD_W then x = LCD_W - 2 end 127 | if y < 0 then yP = 1 end 128 | if y >= LCD_W then y = LCD_H - 2 end 129 | lcd.drawLine(x - 1, y - 1, x - 1, y + 1, SOLID, FORCE) 130 | lcd.drawLine(x , y - 1, x , y + 1, SOLID, FORCE) 131 | lcd.drawLine(x + 1, y - 1, x + 1, y + 1, SOLID, FORCE) 132 | end 133 | 134 | local function drawObject(object, markerFlag) 135 | x = object.x - drone.x 136 | y = object.y - drone.y 137 | z = object.z - drone.z 138 | if object.t == "gateGround" then 139 | xDispLeft = ((x - gate.w/2) * zScale) / z + LCD_W/2 140 | xDispRight = ((x + gate.w/2) * zScale) / z + LCD_W/2 141 | yDispTop = ((y - gate.h) * zScale) / z + LCD_H/2 142 | yDispBottom = ((y + 0) * zScale) / z + LCD_H/2 143 | xDispMarker = (x * zScale) / z + LCD_W/2 144 | yDispMarker = ((y - gate.h/2) * zScale) / z + LCD_H/2 145 | drawLine(xDispLeft, yDispBottom, xDispLeft, yDispTop, 'v') 146 | drawLine(xDispRight, yDispBottom, xDispRight, yDispTop, 'v') 147 | drawLine(xDispLeft, yDispTop, xDispRight, yDispTop, 'h') 148 | elseif object.t == "gateAir" then 149 | xDispLeft = ((x - gate.w/2) * zScale) / z + LCD_W/2 150 | xDispRight = ((x + gate.w/2) * zScale) / z + LCD_W/2 151 | yDispTop = ((y - gate.h*2) * zScale) / z + LCD_H/2 152 | yDispMid = ((y - gate.h) * zScale) / z + LCD_H/2 153 | yDispBottom = ((y + 0) * zScale) / z + LCD_H/2 154 | xDispMarker = (x * zScale) / z + LCD_W/2 155 | yDispMarker = ((y - gate.h*3/2) * zScale) / z + LCD_H/2 156 | drawLine(xDispLeft, yDispBottom, xDispLeft, yDispTop, 'v') 157 | drawLine(xDispRight, yDispBottom, xDispRight, yDispTop, 'v') 158 | drawLine(xDispLeft, yDispTop, xDispRight, yDispTop, 'h') 159 | drawLine(xDispLeft, yDispMid, xDispRight, yDispMid, 'h') 160 | elseif object.t == "flagLeft" then 161 | xDispLeft = ((x - flag.w/2) * zScale) / z + LCD_W/2 162 | xDispRight = ((x + flag.w/2) * zScale) / z + LCD_W/2 163 | yDispTop = ((y - gate.h*2) * zScale) / z + LCD_H/2 164 | yDispMid = ((y - gate.h) * zScale) / z + LCD_H/2 165 | yDispBottom = ((y + 0) * zScale) / z + LCD_H/2 166 | xDispMarker = ((x + flag.w*2) * zScale) / z + LCD_W/2 167 | yDispMarker = ((y - gate.h*3/2) * zScale) / z + LCD_H/2 168 | drawLine(xDispLeft, yDispMid, xDispLeft, yDispTop, 'v') 169 | drawLine(xDispRight, yDispBottom, xDispRight, yDispTop, 'v') 170 | drawLine(xDispLeft, yDispTop, xDispRight, yDispTop, 'h') 171 | drawLine(xDispLeft, yDispMid, xDispRight, yDispMid, 'h') 172 | elseif object.t == "flagRight" then 173 | xDispLeft = ((x - flag.w/2) * zScale) / z + LCD_W/2 174 | xDispRight = ((x + flag.w/2) * zScale) / z + LCD_W/2 175 | yDispTop = ((y - gate.h*2) * zScale) / z + LCD_H/2 176 | yDispMid = ((y - gate.h) * zScale) / z + LCD_H/2 177 | yDispBottom = ((y + 0) * zScale) / z + LCD_H/2 178 | xDispMarker = ((x - flag.w*2) * zScale) / z + LCD_W/2 179 | yDispMarker = ((y - gate.h*3/2) * zScale) / z + LCD_H/2 180 | drawLine(xDispLeft, yDispBottom, xDispLeft, yDispTop, 'v') 181 | drawLine(xDispRight, yDispMid, xDispRight, yDispTop, 'v') 182 | drawLine(xDispLeft, yDispTop, xDispRight, yDispTop, 'h') 183 | drawLine(xDispLeft, yDispMid, xDispRight, yDispMid, 'h') 184 | end 185 | if markerFlag then 186 | drawMarker(xDispMarker, yDispMarker) 187 | end 188 | end 189 | 190 | local function generateObject() 191 | objectCounter = objectCounter + 1 192 | distance = objectCounter * zObjectsStep 193 | object = {x = math.random(-track.w, track.w), y = 0, z = distance} 194 | typeId = math.random(1,6) 195 | if typeId == 1 or typeId == 2 then 196 | object.t = "gateGround" 197 | elseif typeId == 3 or typeId == 4 then 198 | object.t = "gateAir" 199 | elseif typeId == 5 then 200 | object.t = "flagRight" 201 | object.x = - math.abs(object.x) - track.w 202 | elseif typeId == 6 then 203 | object.t = "flagLeft" 204 | object.x = math.abs(object.x) + track.w 205 | end 206 | return object 207 | end 208 | 209 | local function init_func() 210 | if lowFps then 211 | rollScale = rollScale / 2 212 | pitchScale = pitchScale / 2 213 | throttleScale = throttleScale / 2 214 | end 215 | bestResult = loadBestResult() 216 | if LCD_W >= 480 then 217 | FORCE = 0 -- override macro not defined in 480x272 lcd 218 | highRes = 1 219 | zScale = zScale * 4 220 | objectsN = objectsN * 2 221 | end 222 | end 223 | 224 | local function run_func(event) 225 | if not raceStarted then 226 | lcd.clear() 227 | lcd.drawText(LCD_W/2 - 59 - 20*highRes, LCD_H/2 + 22, "Press [Enter] to start") 228 | if counter then 229 | lcd.drawText(LCD_W/2 - 27 - 10*highRes, LCD_H/2 - 4, "Result:") 230 | lcd.drawNumber(LCD_W/2 + 12 + 14*highRes, LCD_H/2 - 4, counter, BOLD) 231 | if isNewBest then 232 | lcd.drawText(LCD_W/2 - 42 - 20*highRes, LCD_H/2 - 30, "New best score!") 233 | else 234 | lcd.drawText(LCD_W/2 - 37 - 15*highRes, LCD_H/2 - 30, "Best score:") 235 | lcd.drawNumber(LCD_W/2 + 26 + 15*highRes, LCD_H/2 - 30, bestResult) 236 | end 237 | else 238 | lcd.drawText(LCD_W/2 - 47 - 25*highRes, LCD_H/2 - 4, "Lua FPV Simulator", BOLD) 239 | end 240 | if event == EVT_ENTER_BREAK then 241 | drone.x = 0 242 | drone.y = 0 243 | drone.z = 0 244 | objectCounter = 0 245 | for i = 1, objectsN do 246 | objects[i] = generateObject(zObjectsStep * i) 247 | end 248 | counter = 0 249 | countDown = 3 250 | startTime = getTime() + countDown * 100 251 | finishTime = getTime() + (raceTime + countDown) * 100 252 | countDown = countDown + 1 253 | startTonePlayed = false 254 | raceStarted = true 255 | isNewBest = false 256 | end 257 | else 258 | if lowFps then 259 | fpsCounter = fpsCounter + 1 260 | if fpsCounter == 2 then 261 | fpsCounter = 0 262 | return 0 263 | end 264 | end 265 | lcd.clear() 266 | currentTime = getTime() 267 | if currentTime < startTime then 268 | local cnt = (startTime - currentTime) / 100 + 1 269 | if cnt < countDown then 270 | playTone(1500, 100, 0) 271 | countDown = countDown - 1 272 | end 273 | lcd.drawNumber(LCD_W/2 - 2, LCD_H/2 + 16, cnt, BOLD) 274 | elseif currentTime < finishTime then 275 | if (currentTime - startTime) < 100 then 276 | lcd.drawText(LCD_W/2 - 6, LCD_H/2 + 16, 'GO!', BOLD) 277 | if not startTonePlayed then 278 | playTone(2250, 500, 0) 279 | startTonePlayed = true 280 | end 281 | end 282 | speed.x = getValue('ail') / rollScale 283 | speed.z = getValue('ele') / pitchScale + minSpeed 284 | speed.y = getValue('thr') / throttleScale 285 | if speed.z < 0 then speed.z = 0 end 286 | drone.y = drone.y - speed.y 287 | if drone.y >= 0 then 288 | drone.y = 0 289 | speed.z = 0 290 | speed.x = 0 291 | end 292 | drone.z = drone.z + speed.z 293 | drone.x = drone.x + speed.x 294 | if drone.x > track.w * 3 then drone.x = track.w * 3 end 295 | if drone.x < -track.w * 3 then drone.x = -track.w * 3 end 296 | if drone.y < -track.h then drone.y = -track.h end 297 | else 298 | if (not bestResult) or (counter > bestResult) then 299 | isNewBest = true 300 | saveBestResult(counter) 301 | bestResult = counter 302 | end 303 | raceStarted = false 304 | end 305 | remainingTime = (finishTime - currentTime)/100 + 1 306 | if remainingTime > raceTime then remainingTime = raceTime end 307 | lcd.drawTimer(LCD_W - 25 - 22*highRes, 2, remainingTime) 308 | local closestDist = drone.z + zObjectsStep * objectsN 309 | for i = 1, objectsN do 310 | if objects[i].z < closestDist and objects[i].z > (drone.z + speed.z) then 311 | closestN = i 312 | closestDist = objects[i].z 313 | end 314 | end 315 | for i = 1, objectsN do 316 | if drone.z >= objects[i].z then 317 | success = false 318 | if objects[i].t == "gateGround" then 319 | if (math.abs(objects[i].x - drone.x) <= gate.w/2) and (drone.y > -gate.h) then 320 | success = true 321 | end 322 | elseif objects[i].t == "gateAir" then 323 | if (math.abs(objects[i].x - drone.x) <= gate.w/2) and (drone.y < -gate.h) and (drone.y > -2*gate.h) then 324 | success = true 325 | end 326 | elseif objects[i].t == "flagLeft" then 327 | if (objects[i].x < drone.x) and (drone.y > -2*gate.h) then 328 | success = true 329 | end 330 | elseif objects[i].t == "flagRight" then 331 | if (objects[i].x > drone.x) and (drone.y > -2*gate.h) then 332 | success = true 333 | end 334 | end 335 | if success then 336 | counter = counter + 1 337 | playTone(1000, 100, 0) 338 | else 339 | counter = counter - 1 340 | playTone(500, 300, 0) 341 | end 342 | objects[i] = generateObject() 343 | else 344 | drawObject(objects[i], i == closestN) 345 | end 346 | end 347 | drawLandscape() 348 | lcd.drawNumber(3, 2, counter) 349 | if event == EVT_EXIT_BREAK then 350 | raceStarted = false 351 | counter = nil 352 | end 353 | end 354 | return 0 355 | end 356 | 357 | return { init=init_func, run=run_func } 358 | -------------------------------------------------------------------------------- /images/scr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/17c2a1fa0cba73a5708ea2cab4d1a906bd4c8a36/images/scr1.png -------------------------------------------------------------------------------- /images/scr2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/17c2a1fa0cba73a5708ea2cab4d1a906bd4c8a36/images/scr2.png -------------------------------------------------------------------------------- /images/scr3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexeystn/lua-fpv-sim/17c2a1fa0cba73a5708ea2cab4d1a906bd4c8a36/images/scr3.png --------------------------------------------------------------------------------