Cerb + Serial +N18 = out of memory

Anyone want to give me some pointers on what I might try to clean up my memory usage? I am pretty sure there’s something I’m doing in the use of the first class that’s causing me to not dereference strings and they linger through GC’s, but I can’t spot it…
Sorry about the huge code posting and very wide question :frowning: :naughty:


namespace Poundy.Devices
{
    public class EMIC2
    {
        private static SerialPort emic;
        private static byte[] readBuf = new byte[1];


        public EMIC2()
        {
            emic = new SerialPort("com1", 9600, Parity.None, 8, StopBits.One);
            setupEmic();
        }

        public EMIC2(string portname)
        {
            emic = new SerialPort(portname, 9600, Parity.None, 8, StopBits.One);
            setupEmic();
        }

        private void setupEmic()
        {
            //            emic.ReadTimeout = Timeout.Infinite;
            //            emic.WriteTimeout = Timeout.Infinite;
            emic.ReadTimeout = 500;
            emic.WriteTimeout = 500;
            try
            {
                emic.Open();
            }
            catch
            {
                throw new Exception("Unhandled failure to open serial port");
            }


        }

        public bool isConnected()
        {
            if (emic != null)
            {
                return true;
            }
            else
                return false;
        }

        public bool isReady()
        {
            if (emic != null)
            {
                // wait for serial ":" or send CR and get ":" back
                return true;
            }
            else
                return false;
        }

        public bool Say(string sentence)
        {
            if (this.isConnected())
            {
                while (sentence.Length > 1020)
                {
                    string sub;
                    sub = sentence.Substring(0, 1020);
                    sendString(sub);
                    sentence = sentence.Substring(1021, sentence.Length - 1020);
                }
                sendString("S" + sentence);
                Debug.GC(true);
                return true;
            }
            else
                return false;
        }

        public void changeVoice(int voice)
        {
            if (this.isConnected())
            {
                //send N
                //wait for ":"

                if (voice > 8)
                    voice = 8;
                else if (voice < 0)
                    voice = 0;

                string buffer = "N" + voice.ToString();
                sendString(buffer);
            }
        }

        public void changeLanguage(int lang)
        {
            if (this.isConnected())
            {
                //send L
                //wait for ":"

                if (lang > 2)
                    lang = 2;
                else if (lang < 0)
                    lang = 0;

                string buffer = "L" + lang.ToString();
                sendString(buffer);
            }
        }

        public void changeVolume(int volume)
        {
            if (this.isConnected())
            {
                //send V
                //wait for ":"

                if (volume > 18)
                    volume = 18;
                else if (volume < -48)
                    volume = -48;

                string buffer = "V" + volume.ToString();
                sendString(buffer);
            }
        }

        public void changeSpeed(int speed)
        {
            if (this.isConnected())
            {
                //send W
                //wait for ":"

                if (speed > 600)
                    speed = 600;
                else if (speed < 75)
                    speed = 75;

                string buffer = "W" + speed.ToString();
                sendString(buffer);
            }
        }

        public void changeParser(int parser)
        {
            if (this.isConnected())
            {
                //send P

                if (parser != 0 || parser != 1)
                    parser = 1;

                string buffer = "P" + parser.ToString();
                sendString(buffer);
            }
        }

        private bool sendString(string str)
        {
            string buffer = str + "\r\n";
            Debug.Print("Sending:");
            Debug.Print(buffer);
            emic.Write(UTF8Encoding.UTF8.GetBytes(buffer), 0, buffer.Length);
            // wait for the ":" after processing the instruction
            bool returnVal = ReadLine(emic, (int)':');
            Debug.GC(true);
            if (returnVal == true)
                Debug.Print("readval True ");
            else
                Debug.Print("readval false");

            return returnVal;

        }
        public static bool ReadLine(SerialPort port, int delim = 10)
        {
            //CR 13 LF 10 = "\r\n"
            bool keepGoing = true;
            int curPos = 0;
            readBuf[curPos] = 0;
            int read = 0;

            while (keepGoing && readBuf[curPos] != delim)
            {
                read = port.Read(readBuf, curPos, 1);
                if (read > 0)
                {
                    Debug.Print("Read= " + (char)readBuf[curPos]);
                    if (readBuf[curPos] == delim)
                    {
                        // we got a delimeter; it might only be in a stream of chars, so try a read-ahead
                        keepGoing = false;
                        read = port.Read(readBuf, curPos, 1);
                        if (read != 0)
                        {
                            // if we read a character here, the timeout didn't come into play, therefore the delimeter we picked up was not the terminator and we have to keep reading the serial port
                            Debug.Print("Read_ = " + (char)readBuf[curPos]);
                            keepGoing = true;
                        }
                    }
                }
            }
            Debug.GC(true);

            if (readBuf[curPos] == delim)
                return true;
            else
                return false;

        }



    }
}



    public partial class Program
    {
        Font f = Resources.GetFont(Resources.FontResources.NinaB);

        EMIC2 myEmic;

        GT.Timer timer = new GT.Timer(1000);
        static bool RoastState;
        static DateTime RoastStart;
        static DateTime RoastEnd;
        static DateTime Roast;
        static TimeSpan Duration;
        static string MyTimer;

        GT.Socket MultiIO;
        InterruptPort btn1_Engage_Rly;
        InputPort btn2_StartStop;
        InputPort btn3_Event;
        OutputPort btn1_Rly_Control;

        // This method is run when the mainboard is powered up or reset.   
        void ProgramStarted()
        {
            /*******************************************************************************************
            Modules added in the Program.gadgeteer designer view are used by typing 
            their name followed by a period, e.g.  button.  or  camera.
            
            Many modules generate useful events. Type +=<tab><tab> to add a handler to an event, e.g.:
                button.ButtonPressed +=<tab><tab>
            
            If you want to do something periodically, use a GT.Timer and handle its Tick event, e.g.:
                GT.Timer timer = new GT.Timer(1000); // every second (1000ms)
                timer.Tick +=<tab><tab>
                timer.Start();
            *******************************************************************************************/
            Utility.SetLocalTime(new DateTime(2013, 11, 17, 12, 00, 00));


            timer.Tick += timer_Tick;
            timer.Start();

            joystick.JoystickPressed += joystick_JoystickPressed;

            myEmic = new EMIC2("com2");
            myEmic.changeVolume(12);
            myEmic.changeVolume(13);
            myEmic.changeVoice(3);
            
            disp.SimpleGraphics.DisplayTextInRectangle("Coffee Roaster Control", 0,0,disp.Width,40, GT.Color.Red, f, GTM.Module.DisplayModule.SimpleGraphicsInterface.TextAlign.Center, GTM.Module.DisplayModule.SimpleGraphicsInterface.WordWrap.Wrap, GTM.Module.DisplayModule.SimpleGraphicsInterface.Trimming.WordEllipsis, GTM.Module.DisplayModule.SimpleGraphicsInterface.ScaleText.None);

            MultiIO = GT.Socket.GetSocket(7, true, null, "Multi");
            btn1_Engage_Rly = new InterruptPort(MultiIO.ReservePin(Gadgeteer.Socket.Pin.Three, null), true, Port.ResistorMode.PullUp, Port.InterruptMode.InterruptEdgeLow);
            btn2_StartStop = new InputPort(MultiIO.ReservePin(Gadgeteer.Socket.Pin.Four, null), false, Port.ResistorMode.PullUp);
            btn3_Event = new InputPort(MultiIO.ReservePin(Gadgeteer.Socket.Pin.Five, null), false, Port.ResistorMode.PullUp);
            btn1_Rly_Control = new OutputPort(MultiIO.ReservePin(Gadgeteer.Socket.Pin.Eight, null), false);

            btn1_Engage_Rly.OnInterrupt += btn1_Engage_Rly_OnInterrupt;

            Debug.Print("Program Started");
        }

        void btn1_Engage_Rly_OnInterrupt(uint data1, uint data2, DateTime time)
        {
            if (RoastState == false)
            {
                Debug.Print("Roast Starting " + DateTime.Now.ToString());
                RoastState = true;
                RoastStart = DateTime.Now;
                disp.Clear();
                myEmic.Say("Starting roast");
                Debug.GC(true);
            }
            else
            {
                Debug.Print("Roast Ending " + DateTime.Now.ToString());
                RoastState = false;
                RoastEnd = DateTime.Now;
                myEmic.Say("Roast done");
                Duration = RoastEnd - RoastStart;
                myEmic.Say("Roast took " + Duration.Minutes.ToString() + " minutes and " + Duration.Seconds.ToString() + " seconds");
                Debug.GC(true);

            }
        }

        void joystick_JoystickPressed(Joystick sender, Joystick.JoystickState state)
        {
            Debug.Print("Joystick pressed - nothing happens here !"); 
        }

        void timer_Tick(GT.Timer timer)
        {
            PulseDebugLED();
            if (RoastState == true)
            {
                Duration = DateTime.Now - RoastStart;
                if (Duration.Minutes<10)
                    MyTimer="0"+Duration.Minutes.ToString();
                else
                    MyTimer=Duration.Minutes.ToString();

                if (Duration.Seconds<10)
                    MyTimer+=":0"+Duration.Seconds.ToString();
                else
                    MyTimer+=":"+Duration.Seconds.ToString();

                if (Duration.Seconds == 0 && Duration.Minutes>0)
                {
                    myEmic.Say(Duration.Minutes.ToString() + " mins");
                }
                
                Debug.Print("Dur: " + MyTimer);

                disp.SimpleGraphics.DisplayRectangle(GT.Color.DarkGray, 2, GT.Color.Black, 0, 40, disp.Width, 60);
   
                disp.SimpleGraphics.DisplayTextInRectangle(MyTimer, 0, 40, disp.Width, 60, GT.Color.Blue, f, GTM.Module.DisplayModule.SimpleGraphicsInterface.TextAlign.Center, GTM.Module.DisplayModule.SimpleGraphicsInterface.WordWrap.Wrap, GTM.Module.DisplayModule.SimpleGraphicsInterface.Trimming.CharacterEllipsis, GTM.Module.DisplayModule.SimpleGraphicsInterface.ScaleText.None);
            }
            Debug.GC(true);

        }
    }
}

Biggest improvement: disable all N18 display usage :slight_smile: (should that be :frowning: )

Commented out the text display and now I don’t get any OOM exceptions. I am not even allocating a bitmap myself, don’t have one loaded as a resource, only the fonts, but am using simplegraphics to display the text.

This brings me to an interesting observation. Is display.Clear() meant to work ? If I display something and then call clear(), the text comes back. In my case, I’ve used the DisplayRectangle() to overwrite the previous timer text and then use DisplayTextInRectangle() to render the text. If I called clear() then DisplayTextInRectangle() the display would not clear and the text would overwrite itself and not be readable.

Brett, I noticed that you are calling Debug.GC(true). Debug.Print the return value to see how much free memory you are ending up with after the free. Also, do that a program startup to see how much you had when it started.

SimpleGraphics is allocating a bitmap internally which represents the screen, so this will be a pretty big memory saving.

Display_N18.Clear() clears the screen, but not the internal bitmap maintained by SimpleGraphics. You can use Display_N18.SimpleGraphics.Clear() to clear the internal bitmap.

Another thing you should watch for, and since I am not running the code I cannot confirm that this is actually an issue in this case, the timer fires every second if the handler takes longer than a second to run, the timer requests will start queuing up and eventually result in an allocation failure. I typically prefer to stop the timer and then restart it once the handler has completed.

Alternative to SimpleGraphics to get text onto screen?

There are two different approaches I can think of off the top of my head.

You can use the native SPI interface to render directly to the display. Two options would be.
[ol]Use a character size bitmap and then render that to the display directly, ie. character by character.
Use a custom bitmap font that you can render character by character to the display.[/ol]

The N18 driver provides all the utility functions you need to do the above reasonably efficiently, unfortunately they are all private but that is not an insurmountable problem.

I have not actually done this with the N18 + Cerb, but the Game-O is similar is some respects, those that know the native interface to the Game-O display will jump on me here, but with regard to clip areas etc. it is similar. I have implemented option 2 in native code on the Game-O when using the 320x240 display resolution, of course managed code and the fact that it is via SPI will be slower, but it should work.

Thanks @ Taylorza. One other option I can think of is clone the driver locally and shorten the displayable area, it’s not like I’m (currently) using a lot of the text :slight_smile:

Glad it’s a rainy day here in Sydney, otherwise I’d be in trouble because I have to roast coffee as soon as I can, and this app is going to run the motor for me - and because of this it’s progressing slower than expected.

@ Brett - I did a quick implementation of option 1, just to see the performance and it is actually not too bad. But I did one subsequent optimization and that was to dynamically create a bitmap that is sized to the text string being written.

Btw. With SimpleGraphics I was running at about 34KB free, with the code below it is just under 75KB.


using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation.Media;
using GT = Gadgeteer;
using Gadgeteer.Modules.GHIElectronics;

namespace CerbN18
{
    public partial class Program
    {
        Font _fnt;
        GT.Timer _timer;
        TextRenderer _renderer;
        
        void ProgramStarted()
        {
            _fnt = Resources.GetFont(Resources.FontResources.NinaB);

            _renderer = new TextRenderer(display_N18);

            _timer = new GT.Timer(1000);
            _timer.Tick += timer_Tick;
            _timer.Start();

            // Use Debug.Print to show messages in Visual Studio's "Output" window during debugging.
            Debug.Print("Program Started");
        }

        void timer_Tick(GT.Timer timer)
        {
            display_N18.Clear();
            _renderer.DrawString("Time:" + DateTime.Now.ToString("hh:mm:ss"), _fnt, GT.Color.Cyan, 0, 0);
            _renderer.DrawString("Memory:" + Debug.GC(false), _fnt, GT.Color.Red, 0, 20);
        }
    }

    public class TextRenderer
    {
        private Display_N18 _display;

        public TextRenderer(Display_N18 display)
        {
            _display = display;
        }

        public void DrawString(string text, Font font, Color color, uint x, uint y)
        {
            int width;
            int height;
            font.ComputeExtent(text, out width, out height);
            using (Bitmap bmp = new Bitmap(width, height))
            {
                bmp.DrawText(text, font, color, 0, 0);
                _display.Draw(bmp, x, y);
            }
           
        }
    }
}


1 Like

Dude, you’re a legend. Push that to codeshare !!

I had, at times, failed allocations of ~41k (yes, 41K) when using SimpleGraphics. Now, like you, I’m still back up over 71k free and haven’t had an issue.

And there’s no speed issue for me, I am only doing updates at 1hz, so it’s a piece of cake.

@ Brett - I am glad that was useful.

I did not really think about putting it onto codeshare, but since it is useful I can do that. However, I will wait, you might have a few enhancements/improvements, in which case it would be better for you to post that version to code share.

Now you mention it :wink:

centred text was the first extension I added. If you don’t want ownership and points (I still think you should !) I’ll upload it once I’ve thought about other areas I may need to extend it.

I also wanted to report a bit more detail about the memory situation. As I mentioned, I’d seen failed allocs in the order of 40k. After your code came along I started reporting memory usage as it went, and with the display in the project but no SimpleGraphics used, I was reporting (at stable state) 72,408 bytes available, and when I then added the code to use the DrawString method it only dropped to 72,252 immediately after the calls to display 3 lines of text. Super Sweet.

@ Brett - I am not overly concerned about ownership or the points, I would rather see someting more useful go up on code share. Besides, I am just glad to be able to mess around with some code. If ever I am in your neck of the woods you can buy me a beer and we can call it even.

1 Like

Hello All,

i came up with this code and decided to use it to print a list(menu style), (thanks @ taylorza), but only found a little uncomfortable the flickering caused by the constant clearing of the complete screen, so i made a small modification that can be usefull in the cases that one needs to print each line, and wanted to share it as a small contribution to the project:


using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation.Media;
using GT = Gadgeteer;
using Gadgeteer.Modules.GHIElectronics;

namespace CerbN18
{
    public partial class Program
    {
        Font _fnt;
        GT.Timer _timer;
        TextRenderer _renderer;
        
        void ProgramStarted()
        {
            _fnt = Resources.GetFont(Resources.FontResources.NinaB);

            _renderer = new TextRenderer(display_N18);

            _timer = new GT.Timer(1000);
            _timer.Tick += timer_Tick;
            _timer.Start();

            // Use Debug.Print to show messages in Visual Studio's "Output" window during debugging.
            Debug.Print("Program Started");
        }

        void timer_Tick(GT.Timer timer)
        {
            _renderer.DrawString("Time:" + DateTime.Now.ToString("hh:mm:ss"), _fnt, GT.Color.Cyan, 0, 0);
            _renderer.DrawString("Memory:" + Debug.GC(false), _fnt, GT.Color.Red, 0, 20);
        }
    }

    public class TextRenderer
    {
        private Display_N18 _display;

        public TextRenderer(Display_N18 display)
        {
            _display = display;
        }

        public void DrawString(string text, Font font, Color color, uint x, uint y)
        {
            int width;
            int height;
            font.ComputeExtent(text, out width, out height);
            using (Bitmap bmp = new Bitmap(_display.Width, height))
            {
                bmp.DrawRectangle(Color.Black, 0, (int)x, (int)y, _display.Width, height, 0, 0, Color.Black, 0, 0, Color.Black, 0, 0, 0);
                _display.Draw(bmp, x, y);
            }
            using (Bitmap bmp = new Bitmap(width, height))
            {
                bmp.DrawText(text, font, color, 0, 0);
                _display.Draw(bmp, x, y);
            }
           
        }
    }
}


As you can see it counts with a Black Background Color, but it can be easily modified to accept a parameter, the Flickering is almost entirely eliminated because there is no clearing, just overwriting the block with an “empty” rectangle!

im not really sure if there is a plan to add this to a library, but im sure it can be very useful (the entire Text renderer!)

thanks again people

1 Like