Issue sending spi result to an egui ui

Hi all, would appreciate some help and advice sorting out my Rust code here. I need to display the output from an MCP3002 SPI chip on an egui ui.
The MCP3002 is fed into the spi pins on a Raspberry Pi 5 running Rust. The issue I have faced is that when using the standard spidev crate and
the SPI driver, I cannot send the output, in this case the variable “dog1” to the standard UPDATE function that the egui crate uses, to an egui GUI.
I can send the output to “println” which is no good as I need to display the output from the MCP3002 SPI chip graphically. If I call the
UPDATE function from FULL_DUPLEX then dog1 goes out of scope which actually shouldn’t happen as UPDATE is called before the closing brace of
FULL_DUPLEX. So what I have done (see code below) is to modify the function signature for FULL_DUPLEX to call
egui::Context and eframe::Frame as parameters so that |ui| and the spi output “dog1” are part of the same function and hence dog1 does not now
go out of scope. See code below which compiles, but NO GUI is displayed. I have tried re-writing the code using a standard struct and impl’s but
then I run into the same problem of dog1 going out of scope.
I’d appreciate if someone could put me right and tell me what needs to be done to get the egui ui show and to display my variable “dog1”. I have been
tearing my hair out over this for several weeks now! Code here:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
use std::error::Error;
use std::io;
use eframe::egui;
use egui::Context;
//use eframe::egui::Context;
use eframe::Frame;
use egui::RawInput;
use egui::Ui;
use std::io::prelude::*;
use spidev::{Spidev, SpidevOptions, SpidevTransfer, SpiModeFlags};

//static mut rx_buf: [u8; 2] = [0, 0];//REF: bookmark: How to specify const array in global scope in Rust

fn main() {

let _ = create_spi();

}

fn create_spi() -> io::Result {

let mut spi = Spidev::open("/dev/spidev0.0")?;
let options = SpidevOptions::new()
     .bits_per_word(8)
     .max_speed_hz(40_000)
     .mode(SpiModeFlags::SPI_MODE_0)
     .build();
spi.configure(&options)?;

 let mut ctx = egui::Context::default();
 let mut _frame = eframe::Frame::_new_kittest();
  
 full_duplex(&mut spi, &mut ctx, &mut _frame);
 Ok(spi) 
}

fn full_duplex(spi: &mut Spidev, ctx: &mut egui::Context, _frame: &mut eframe::Frame) {

    let tx_buf = [0x0D, 0x02];
    let mut rx_buf = [4; 2];
    let mut transfer = SpidevTransfer::read_write(&tx_buf, &mut rx_buf);
    let _ = spi.transfer(&mut transfer);
    
    let dog1 =rx_buf[1].to_string();
    let raw_input = egui::RawInput::default();
 
    let _= ctx.run(raw_input, |ctx| {egui::CentralPanel::default().show(ctx, |ui| {
        ui.heading("My egui Application");
        ui.horizontal(|ui| {
        ui.label(dog1.clone()); 
        });
        });
        
    });
   }

Hi, welcome!

To make your question easier to navigate, please edit your post so the code is properly formatted as described in Forum Code Formatting and Syntax Highlighting - meta - The Rust Programming Language Forum. This will help community members read the code to provide any feedback they might have. If there are compiler errors, please also include the full error message, as well.

I can only guess what a "standard egui update function" is. egui itself doesn't have an update function... The eframe::App abstraction does, but it isn't clear from the provided code if you are using this. Assuming you are using the App trait, the struct that implements it needs a reference to everything that needs to be drawn in the UI.

In very broad terms, this almost always means you want the struct to own everything that the UI will interact with. Naively, you could store a String in your app struct, and reading the SPI device would update that string. In turn, when eframe renders the GUI later on, it can read the "cached" string from the app context.

that's not how you run a GUI application. a GUI application needs a event loop, and the ctx.run() must be called each frame, with the correct raw_input.

if you are not familiar with immediate GUI libraries like egui, I suggest you use eframe, which manages the event loop and paints the window for you.

eframe is very easy to use, you just need to implment the eframe::App trait for your application type. the application type is where you store all your data that is "global" to the whole application. the App trait is just the "callback" akin to classic GUI framework, except that, instead of being called like on_button_click(), on_key_pressed(), etc, eframe::App::update() is called every frame.

you should at least read and understand the hello_world example, and preferably, also the documentation of eframe

the application should also manage synchronization of data from external source, such as the SPI data in your case. here's a brief summary what you should do (on top of the "hello_world" example):

  • make sure to use proper synchroniation for your data, e.g. channel::Receiver<MyData>;

  • when you create the App in you AppCreator, save a clone of the egui::Context for data update;

  • when new data arrived, send the data to the application and then wake up the UI thread using the saved egui::Context

  • in the App::update callback, draw the UI with the data

fn main() {
    eframe::run_native(
        "My egui App",
        eframe::NativeOptions::default(),
        Box::new(|cc| {
            let egui_ctx = cc.egui_ctx.clone();
            let (tx, rx) = std::sync::mpsc::channel();
            start_data_capture(egui_ctx, tx);
            Ok(Box::new(MyApp::new(rx)))
        }),
    )
}
fn start_data_capture(egui_ctx: egui::Context, sender: Sender) {
    // this example polls data in a thread, assuming io blocks until data ready
    // you can also do this if your data is delivered in a callback, since
    // both the egui context and channel sender are thread safe
    thread::spawn(move || {
        loop {
            // block until data ready
            let data = io_read();
            // send data to app
            sender.send(data);
            // notify UI to redraw
            egui_ctx.request_repaint();
        }
    });
}
struct MyApp {
    // last received data
    data: MyData,
    // the channel new data will be received
    receiver: Receiver<MyData>,
}
impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
        // redraw can be triggered not only by the data capture thread
        // use `try_recv()` instead of `recv()` to avoid blocking UI thread.
        self.receiver.try_recv() {
            Ok(new_data) => {
                self.data = new_data
            }
            Err(_) => {
                // no new data, render with previous data 
            }
        }
        egui::CentralPanel::default().show(ctx, |ui| {
            let text = format("current data value: {}", self.data);
            ui.label(&text);
        });
    }
}

This is most helpful, educational and informative, and I feel I owe you my life! Thankyou very much. I shall be studying and applying your helpful tuition here over the next few days. Please accept my apologies for the delay in replying - I'm working here in Thailand, and having had one heck of a tussle with Rust over the past few weeks, I had to take 24 hours away from it. Just to let you know I'm an ex Python guy, a semi retired hardware engineer, math teacher and radio ham (GM6LDG) and wanted the most up to date coding language to get competent in. I'm hoping to be able to get myself up to a level of skill to be able to get some online programming development work. After Python, I'm finding myself putting my brain into a contortionist state in order to get used to Rust! But thanks to kind guys like you, I'm turning the corner! Thanks once again, very much appreciated!