Flight radar (OLED stress test)
A compute-dense radar scope running real compiled Arduino firmware on a 1.5″ 128×128 OLED. A full framebuffer would swallow the Uno's entire 2 KB of SRAM, so the sketch renders the classic embedded way — 16 raster passes per frame into a 128-byte page buffer, each stripe blitted over I²C at 400 kHz — drawing range rings, a rotating sweep and aircraft blips with trails and callsign labels. Watch REAL air traffic: the Flight feed panel opens with this example — pick an airspace and press Start feed to stream live ADS-B aircraft onto the scope. Or feed it yourself over the serial monitor: P,<id>,<x>,<y>,<vx>,<vy>,<label> places or updates an aircraft, R,<id> removes it, and between updates each one dead-reckons along its last heading.

What's on the bench
- Arduino Uno
- Battery
- OLED 1.5″ 128×128
How it's wired
- Battery · pos→Arduino Uno · 5v
- Battery · neg→Arduino Uno · gnd
- OLED 1.5″ 128×128 · vcc→Arduino Uno · 5v
- OLED 1.5″ 128×128 · gnd→Arduino Uno · gnd2
- Arduino Uno · sda→OLED 1.5″ 128×128 · sda
- Arduino Uno · scl→OLED 1.5″ 128×128 · scl
The code
This Arduino C++ sketch lives in the Code tab; sign in and press Compile & upload to build real firmware for the emulated board.
// Flight radar — Arduino Uno + SH1107 1.5" 128x128 OLED (I2C). // // Draws a radar scope (range rings, rotating sweep, aircraft blips with // motion trails and 3x5-font callsign labels) on a 128x128 monochrome // OLED. A full framebuffer would be 128*128/8 = 2048 bytes — the // ATmega328P's ENTIRE SRAM — so this renders u8g2-style in page mode: // the scene is rasterized 16 times per frame, once per 8-row stripe, // into a 128-byte page buffer that is blitted immediately. More compute // per frame than a framebuffered SSD1306, in less RAM. // // Aircraft positions stream in over serial; between updates each target // is dead-reckoned along its last known velocity. // // Serial protocol (115200 baud, newline-terminated ASCII): // P,<id>,<x>,<y>,<vx>,<vy>[,<label>] upsert aircraft <id> (0..7). // x,y in screen pixels (0..127); vx,vy in 1/16 px per frame. // R,<id> remove aircraft <id>. // C clear all aircraft (a live data // feed sends this first so the boot demo traffic vanishes). // L,<text> scope label, top-left corner // (<=10 chars, A-Z/0-9/space — the live feed sends the airspace's // airport code, e.g. LHR). "L," alone clears it. // Output: F,<frame>,<active>,<ms per frame> — printed when the active // count changes and as a ~7 s heartbeat, NOT every frame (that would be // ~10 lines/s of monitor spam). // // Built with arduino-cli, core arduino:[email protected] (the compile-service // pin). No libraries beyond the bundled Wire — the SH1107 is driven raw // (page addressing, split column pointer, DC-DC via 0xAD — NOT the // SSD1306's 0x8D) so the whole rasterization + blit path is this // sketch's own code. #include <Wire.h> #define OLED_ADDR 0x3D #define W 128 #define H 128 #define MAX_AC 8 #define TRAIL_LEN 8 // Page-mode render target: one 8-row stripe of the display. static uint8_t pageBuf[W]; static uint8_t curPage; // stripe being rasterized, 0..15 struct Aircraft { bool active; int16_t x, y; // position, 12.4 fixed point (pixels * 16) int16_t vx, vy; // velocity, 1/16 px per frame char label[5]; uint8_t trail_head; uint8_t trail_n; uint8_t trail_x[TRAIL_LEN]; // past positions, whole pixels uint8_t trail_y[TRAIL_LEN]; }; static Aircraft ac[MAX_AC]; // ---------------------------------------------------------------- font ---- // 3x5 font, digits + uppercase, column-major, 3 bytes per glyph (bits 0..4). static const uint8_t FONT35[] PROGMEM = { 0x1F, 0x11, 0x1F, // 0 0x12, 0x1F, 0x10, // 1 0x1D, 0x15, 0x17, // 2 0x11, 0x15, 0x1F, // 3 0x07, 0x04, 0x1F, // 4 0x17, 0x15, 0x1D, // 5 0x1F, 0x15, 0x1D, // 6 0x01, 0x01, 0x1F, // 7 0x1F, 0x15, 0x1F, // 8 0x17, 0x15, 0x1F, // 9 0x1E, 0x05, 0x1E, // A 0x1F, 0x15, 0x0A, // B 0x0E, 0x11, 0x11, // C 0x1F, 0x11, 0x0E, // D 0x1F, 0x15, 0x11, // E 0x1F, 0x05, 0x01, // F 0x0E, 0x11, 0x1D, // G 0x1F, 0x04, 0x1F, // H 0x11, 0x1F, 0x11, // I 0x08, 0x10, 0x0F, // J 0x1F, 0x04, 0x1B, // K 0x1F, 0x10, 0x10, // L 0x1F, 0x02, 0x1F, // M (approx) 0x1F, 0x01, 0x1E, // N 0x0E, 0x11, 0x0E, // O 0x1F, 0x05, 0x02, // P 0x0E, 0x19, 0x1E, // Q 0x1F, 0x05, 0x1A, // R 0x12, 0x15, 0x09, // S 0x01, 0x1F, 0x01, // T 0x0F, 0x10, 0x1F, // U 0x07, 0x18, 0x07, // V 0x1F, 0x08, 0x1F, // W (approx) 0x1B, 0x04, 0x1B, // X 0x03, 0x1C, 0x03, // Y 0x19, 0x15, 0x13, // Z }; // 64-entry sine table over the full circle, Q7 (value = sin * 127). static const int8_t SIN64[] PROGMEM = { 0, 12, 25, 37, 49, 60, 71, 81, 90, 98, 106, 112, 117, 122, 125, 126, 127, 126, 125, 122, 117, 112, 106, 98, 90, 81, 71, 60, 49, 37, 25, 12, 0, -12, -25, -37, -49, -60, -71, -81, -90, -98, -106, -112, -117, -122, -125, -126, -127, -126, -125, -122, -117, -112, -106, -98, -90, -81, -71, -60, -49, -37, -25, -12, }; static int8_t isin(uint8_t a) { return (int8_t)pgm_read_byte(&SIN64[a & 63]); } static int8_t icos(uint8_t a) { return isin(a + 16); } // ------------------------------------------------------------ raster ------ // All primitives draw through px(), which clips to the CURRENT PAGE's // 8-row window — the same scene is replayed for every page (u8g2 page // mode). 16-bit coordinates: screen maths on an 8-bit target overflows // int8_t silently, and off-screen points must clip, not wrap. static inline void px(int16_t x, int16_t y) { if ((uint16_t)x >= W || (uint16_t)y >= H) return; if (((uint8_t)y >> 3) != curPage) return; pageBuf[(uint8_t)x] |= (uint8_t)1 << (y & 7); } static void line(int16_t x0, int16_t y0, int16_t x1, int16_t y1) { int16_t dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; int16_t dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; int16_t err = dx + dy; for (;;) { px(x0, y0); if (x0 == x1 && y0 == y1) break; int16_t e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } } static void circle(int16_t cx, int16_t cy, int16_t r) { int16_t x = r, y = 0; int16_t err = 1 - r; while (x >= y) { px(cx + x, cy + y); px(cx - x, cy + y); px(cx + x, cy - y); px(cx - x, cy - y); px(cx + y, cy + x); px(cx - y, cy + x); px(cx + y, cy - x); px(cx - y, cy - x); y++; if (err < 0) err += 2 * y + 1; else { x--; err += 2 * (y - x) + 1; } } } static void glyph(int16_t x, int16_t y, char c) { int8_t idx; if (c >= '0' && c <= '9') idx = c - '0'; else if (c >= 'A' && c <= 'Z') idx = 10 + (c - 'A'); else return; for (int8_t col = 0; col < 3; col++) { uint8_t bits = pgm_read_byte(&FONT35[idx * 3 + col]); for (int8_t row = 0; row < 5; row++) if (bits & (1 << row)) px(x + col, y + row); } } static void text(int16_t x, int16_t y, const char *s) { for (; *s; s++, x += 4) glyph(x, y, *s); } // ------------------------------------------------------------- oled ------- static void cmd(uint8_t c) { Wire.beginTransmission(OLED_ADDR); Wire.write((uint8_t)0x00); Wire.write(c); Wire.endTransmission(); } static void oledInit() { static const uint8_t INIT[] PROGMEM = { 0xAE, // display off 0xDC, 0x00, // display start line 0 (SH1107: takes an argument) 0x81, 0x2F, // contrast 0x20, // page addressing mode (STANDALONE on SH1107) 0xA0, // segment remap normal 0xC0, // COM scan normal 0xA8, 0x7F, // multiplex 128 0xD3, 0x00, // display offset 0xD5, 0x50, // clock 0xD9, 0x22, // precharge 0xDB, 0x35, // VCOM detect 0xAD, 0x8A, // DC-DC on (SH1107's charge pump — NOT the SSD1306 0x8D) 0xA4, // resume from RAM 0xA6, // normal (non-inverted) 0xAF, // display on }; for (uint8_t i = 0; i < sizeof(INIT); i++) cmd(pgm_read_byte(&INIT[i])); } /** Blit the current page buffer to display page `curPage`. */ static void blitPage() { cmd(0xB0 | curPage); // page address cmd(0x00); // column low nibble = 0 cmd(0x10); // column high nibble = 0 for (uint16_t i = 0; i < W; i += 16) { Wire.beginTransmission(OLED_ADDR); Wire.write((uint8_t)0x40); Wire.write(&pageBuf[i], 16); Wire.endTransmission(); } } // ------------------------------------------------------------ traffic ----- static int16_t clampi(int16_t v, int16_t lo, int16_t hi) { return v < lo ? lo : (v > hi ? hi : v); } static void upsert(uint8_t id, int16_t x, int16_t y, int16_t vx, int16_t vy, const char *label) { if (id >= MAX_AC) return; Aircraft &a = ac[id]; if (!a.active) { a.trail_head = 0; a.trail_n = 0; a.label[0] = 0; } a.active = true; a.x = clampi(x, 0, W - 1) << 4; a.y = clampi(y, 0, H - 1) << 4; a.vx = clampi(vx, -160, 160); // |v| <= 10 px/frame keeps vectors on scope a.vy = clampi(vy, -160, 160); if (label && *label) { uint8_t i = 0; for (; i < 4 && label[i]; i++) a.label[i] = label[i]; a.label[i] = 0; } } static void seedDemo() { upsert(0, 30, 24, 22, 9, "BA12"); upsert(1, 100, 110, -18, -6, "AF33"); upsert(2, 64, 12, 4, 14, "KL7"); upsert(3, 20, 100, 16, -12, "EZY9"); upsert(4, 110, 40, -12, 11, "DL04"); upsert(5, 48, 70, 14, 10, "RY88"); } static void stepTraffic() { for (uint8_t i = 0; i < MAX_AC; i++) { Aircraft &a = ac[i]; if (!a.active) continue; // record trail point (whole pixels) before moving a.trail_x[a.trail_head] = (uint8_t)(a.x >> 4); a.trail_y[a.trail_head] = (uint8_t)(a.y >> 4); a.trail_head = (a.trail_head + 1) & (TRAIL_LEN - 1); if (a.trail_n < TRAIL_LEN) a.trail_n++; a.x += a.vx; a.y += a.vy; // bounce off scope edges so demo traffic stays on screen if (a.x < 0) { a.x = 0; a.vx = -a.vx; } if (a.x > (W - 1) << 4) { a.x = (W - 1) << 4; a.vx = -a.vx; } if (a.y < 0) { a.y = 0; a.vy = -a.vy; } if (a.y > (H - 1) << 4) { a.y = (H - 1) << 4; a.vy = -a.vy; } } } // ------------------------------------------------------------- serial ----- static char rxLine[32]; static uint8_t rxLen = 0; static char scopeLabel[11]; static void handleLine(char *s) { if (s[0] == 'C' && s[1] == 0) { for (uint8_t i = 0; i < MAX_AC; i++) ac[i].active = false; return; } if (s[0] == 'L' && s[1] == ',') { uint8_t i = 0; for (; i < sizeof(scopeLabel) - 1 && s[2 + i]; i++) scopeLabel[i] = s[2 + i]; scopeLabel[i] = 0; return; } if (s[0] == 'R' && s[1] == ',') { uint8_t id = (uint8_t)atoi(s + 2); if (id < MAX_AC) ac[id].active = false; return; } if (s[0] != 'P' || s[1] != ',') return; // P,<id>,<x>,<y>,<vx>,<vy>[,<label>] int16_t v[5]; char *p = s + 2; for (uint8_t i = 0; i < 5; i++) { v[i] = (int16_t)atoi(p); char *c = strchr(p, ','); if (!c) { if (i < 4) return; p = NULL; break; } p = c + 1; } upsert((uint8_t)v[0], v[1], v[2], v[3], v[4], p); } static void pollSerial() { while (Serial.available()) { char c = (char)Serial.read(); if (c == '\n' || c == '\r') { if (rxLen) { rxLine[rxLen] = 0; handleLine(rxLine); rxLen = 0; } } else if (rxLen < sizeof(rxLine) - 1) { rxLine[rxLen++] = c; } } } // -------------------------------------------------------------- frame ----- static uint8_t sweep = 0; static uint32_t frame = 0; static uint32_t lastMs = 0; /** Rasterize the whole scene into the CURRENT page's window. */ static void renderPage() { memset(pageBuf, 0, sizeof(pageBuf)); // scope: crosshair + range rings centred mid-screen const int16_t cx = W / 2, cy = H / 2; for (int16_t x = 0; x < W; x += 4) px(x, cy); for (int16_t y = 0; y < H; y += 4) px(cx, y); circle(cx, cy, 20); circle(cx, cy, 40); circle(cx, cy, 60); // rotating sweep line (square panel → a true circle sweep) int16_t sx = ((int16_t)icos(sweep) * 60) >> 7; int16_t sy = ((int16_t)isin(sweep) * 60) >> 7; line(cx, cy, cx + sx, cy + sy); // scope label (live feed's airspace code), top-left corner if (scopeLabel[0]) text(1, 1, scopeLabel); for (uint8_t i = 0; i < MAX_AC; i++) { Aircraft &a = ac[i]; if (!a.active) continue; int16_t x = a.x >> 4, y = a.y >> 4; for (uint8_t t = 0; t < a.trail_n; t++) px(a.trail_x[(a.trail_head - 1 - t) & (TRAIL_LEN - 1)], a.trail_y[(a.trail_head - 1 - t) & (TRAIL_LEN - 1)]); // aircraft blip: 3x3 diamond + velocity vector px(x, y); px(x - 1, y); px(x + 1, y); px(x, y - 1); px(x, y + 1); line(x, y, x + (a.vx >> 3), y + (a.vy >> 3)); text(x + 3, y - 6, a.label); } } static void render() { // u8g2-style page loop: rasterize the scene 16 times, one 8-row // stripe at a time, blitting each stripe as it completes. for (curPage = 0; curPage < H / 8; curPage++) { renderPage(); blitPage(); } sweep++; uint8_t active = 0; for (uint8_t i = 0; i < MAX_AC; i++) if (ac[i].active) active++; // Stats only when something changed (plus a slow heartbeat) — a // per-frame print would flood the serial monitor at ~10 lines/s. static uint8_t lastActive = 255; uint32_t now = millis(); frame++; if (active != lastActive || (frame & 63) == 0) { lastActive = active; Serial.print(F("F,")); Serial.print(frame); Serial.print(','); Serial.print(active); Serial.print(','); Serial.println(now - lastMs); } lastMs = now; } void setup() { Serial.begin(115200); Wire.begin(); Wire.setClock(400000L); oledInit(); seedDemo(); Serial.println(F("FLIGHT RADAR READY")); } void loop() { pollSerial(); stepTraffic(); render(); }



