2025. december 30., kedd

Log No.4 of the 25/26 Hungarian National Engineering Olympiad

 Blog time!

It's only been 7 days and 27 minutes since my last blog post (yup, it's 1AM again), but I've got plenty of material.

First of all, I did manage to fix the subprocess issue that was bugging me for 3 days; like this:

                ParseSerial = subprocess.Popen( #Open and reopen every loop
                ["resources\\ParseSerial.exe"],
                stdout=subprocess.PIPE,
                stdin=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
                )
                 ParseSerial.stdin.write(data)
                 ParseSerial.stdin.flush()
                 returndata = ParseSerial.communicate()
                 print(returndata)

so it basically closes down and reopens the PIPE every loop (I guess forcing the stdin and stdout, because ParseSerial.communicate() closes the IO and gives a bunch of fun errors).
But in the end, I didn't end up using it. I realized how I don't really need much receiver-side parsing, because I can just send already easily-interpretable data from the rocket. So I switched to putting the received data into a .JSON file and reading from that.

Speaking of JSON, I added a config tab in the GUI, where you can set API key, COM channel, baud rate, and refresh frequency, so it doesn't require you to change the JSON manually or go digging around in the code and rebuild it (eww) and you can change it during runtime.

I also now have a button that you press to start sampling, and it calls all scripts needed to keep the data updated. Though this was a bit tricky, because, for the JS scripts, I first tried using await Neutralino.os.execCommand(`node ${pathtoscript}`) , which didn't work because node doesn't recognize Neutralino commands  (like await Neutralino.init() ), so I looked it up and got some info from Stackoverflow to import the JS script I want to run and call it as a function. And that worked. I also needed to call a python script to get the actual serial data, which I also did with 
await Neutralino.os.execCommand(`python &{pythonpath}`) . For a while this didn't work (even when called with 'python3'), until it did. I'm not sure what I did, but the errors went away and it can call, it now but what's weird  is that I got completely different errors when running with python or python3.

Then I noticed a big issue. When you build the app with 'neu build', it puts spews out a .exe and a resources.neu. But so far I got the paths to the Json, JS and python files by getting the absolute path and then adding "./SerialRead.py" to the end, or something along the lines. But because there are no more 'independent' files after building, you can't find the path, let alone call it, because its all in resources.neu. So I tried using the built-in NL_PATH command, which you would THINK would work because its meant for this, but it gives a single dot as path. Because of this, Instead decided to put the built app, resources.neu in a folder, along with two JSON files with the same name. Now, when editing the code, it checks whether a variable called 'const build' is set to true or false. If it's set to false, it uses the default path through the ./resources folder (so you can do 'neu run'), but if set to true, it uses the exposed json files in the folder (for the built app). I also did this with the python script, because that's not in the resources.neu file either.


I also got the RPi CM5 finally! Along with the IO board. I was really looking forward to it, because it's literally a mini PC running linux, but needless to say for 4 days I thought it was broken on arrival because I didn't know you had to attach it to the IO board with force, not just place it on there. Obvious in retrospect, but I didn't want to break it. Anyways, I'm gonna have to cancel that replacement request...
So far, it's what you'd expect; awesome af, but I haven't done much with it yet, because I haven't gotten around to making a 5V-->3.3V level shifter for UART so I can actually try and read some input without having to buy magic smoke refill.

Back to the SerialRead.py; as I mentioned, I'm not using pipes or subprocess, but changing a json file constantly. Which works, but the JS file supposed to read and display the values can't read it. And I don't get it, because the writing time of python and reading period of JS does definitely not overlap, but especially not forever. It reads it once, then stops doing so (it's in an infinite loop). But if I disconnect the Serial-to-USB adapter, killing the python script, it stars updating again and displaying the newest data. I'm really not sure what to do about this, but I'll figure it out, maybe giving the json data directly to the JS script. I'll see.

Oh and I also got an ST-LINK V2 so now I can get to coding that finally. I'm really doing anything but resting in the winter break, but I love these projects so much, this one especially.

I'm pretty sure I had like 3 more things I wanted to share, but I can't remember, so bye now.

TTZ

2025. december 23., kedd

Log No.3 of the 25/26 Hungarian National Engineering Olympiad

 


Okay, so I don't really have enough new material to make this blog post, but I'm experiencing some of the weirdest errors and behavior from my python code that gets Serial input and passes it to a string parsing C++ exe. I SHOULD be debugging it but I'm so flabbergasted that I have to share this right now.

But before that, I mentioned in my last blog how I was trying to rewrite the PIC code to make it shorter by like ~150 lines, but couldn't do that because I didn't know how to increment binary. Well, as it turns out, its literally just that. uint8_t Var = 0b00000 and then Var++. That's it, the MCU can handle it. Really tried to overcomplicate that one. Anyways, I managed to go from writing this code out 15 times

    ADCON0bits.CHS = 0b00000; //Select AN0
    __delay_ms(3); //Delay for channel switching
    ADCON0bits.GO_nDONE = 1; //Start sampling
    while(ADCON0bits.GO_nDONE == 1){ //Wait until sampling is done.
        
    }
    A0result = ((uint16_t)ADRESH << 8) | ADRESL;

to this:

    uint16_t results[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};    
    uint8_t ChSEL = 0b00000;
    char label[6];   // "A14:\0"
    label[0] = 'A';
    
    while(1){
        for(int i = 0; i <= 14; i++){
            ADCON0bits.CHS = ChSEL; //Select Channel
            __delay_ms(3); //Delay for channel switching
            ADCON0bits.GO_nDONE = 1; //Start sampling
            while(ADCON0bits.GO_nDONE == 1){ //Wait until sampling is done.

            }
            results[i] = ((uint16_t)ADRESH << 8) | ADRESL;
            
            if (ChSEL < 10) {
                label[1] = '0';
                label[2] = '0' + ChSEL;
                label[3] = ':';
                label[4] = '\0';
            } else {
                label[1] = '0' + (ChSEL / 10);
                label[2] = '0' + (ChSEL % 10);
                label[3] = ':';
                label[4] = '\0';
            }

            dectohex(results[i], label);
              ChSEL++;
    
        }
        ChSEL = 0b00000;
       __delay_ms(950);

    }
}

Much cleaner, I'd argue. 
Another thing I want to somehow rewrite, is that, currently, the ADC values get passed to UART (dectohex function) as hex codes, so it has to get decoded at the receiving computer. Which is fine, it's not that resource-intensive to turn hex to integers (CPUs like bitshifting), but it's still inconvenient. I've been using HEX because it's easier to handle as a string (well, more accurately, as an array of chars because it's C), as it doesn't need typecasting binary->int->char. But I would like to fix that and have the ADC values passed as integers so it doesn't need decoding from hex.

SECONDLY.
I tried using Windows API to get the serial input to display the data in the OpenApollo GUI, because it's low-level C and runs fast. But working with Windows API makes me want to commit unspeakable acts (I swear, this OS is held together by 30 year-old duct tape. Like DWORD is a 32bit integer because we REEALLYY need backwards compatibility to windows 98). So I gave up on that because python is fast enough.
Getting serial is easy enough, and I'm using subprocess to pass that data to the .exe. Right now, I'm only running some test script

int main() {
    std::string a;
    std::getline(std::cin, a);
    std::cout << "Hello World from C++. Got:" << a << std::endl;
    return 0;
}
//I don't like using namespace for std so I see what is std library
and I'm getting a multitude of really weird errors from python. Here's the incomplete list (I'm using subprocess.Popen):
I get the data from serial but it can't pass it because they're bytes, and it needs string. Fine. Easy fix.
    data = ser.readline().decode('ascii', errors='ignore')
Then I write this:
    AnalogParse.stdin.write(data)
It gives me a ValueError of: I/O operation on closed file. 
SO I change it to this
    data = ser.readline().decode(encoding='ascii')
No error. I don't know why, but let's move on. I need this line:
    AnalogParse.stdin.flush()
    AnalogParse.stdin.close()
And... it doesn't recognize the flush command? It did when I was running a test.py to figure out subprocess. It points to the parentheses with 'Invalid argument'. But flush() does NOT TAKE ARGUMENTS. With OS error 22: The device does not recognize the command.
Thanks Windows, very helpful.
Also this: Exception ignored in: <_io.TextIOWrapper name=3 encoding='cp1252'>
So I remove it. Now, I get the same error with
    AnalogParse.stdin.close()
Sure, I'Il remove it, I don't need it inside the loop (I think).
I had some more errors with 'I/O operation on closed file' on the flush, but I can't recreate it so I'Il just try to remember the situation.
Because of this error, I added this line
print(type(AnalogParse.stdin.flush))
And I get:
<class 'builtin_function_or_method'>
So this has absolutely NOTHING to do with I/O
But fine, I'Il import io. Still the same error, still the same type.
I remove everything unnecessary. Now, the code does run. But it gives me empty lines. The terminal opens, and it tries to write, but it just keeps writing empty lines forever. And then, after a while, it exits because of this line:
    AnalogParse.stdin.write(data)
with 'data' being an invalid argument.  I just cannot figure out why. Here's the code so far
import serial
import subprocess

ser = serial.Serial(
    port='COM4',
    baudrate=9600,
    parity=serial.PARITY_ODD,
    stopbits=serial.STOPBITS_TWO,
    bytesize=serial.SEVENBITS
)

AnalogParse = subprocess.Popen(
    ["ParseLocation.exe"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
    shell = True
    )

while(True):
    data = ser.readline().decode(encoding='ascii')
    AnalogParse.stdin.write(data)
    print(AnalogParse.stdout.readline())
I do not understand.
On a side note, I also tried to remove shell=true and give it a local route, because shell adds a lot of overhead, but it just can't find it, even if I add the full C:\ path.

That's all for now.

Tamás Zétény Tóth
TTZ 

*at the PIC section, I meant float instead of int, but you get it.
 
Update: It's 4 months later, and in hindsight, these errors aren't quite deserving of the title "some of the weirdest errors and behavior". But in my defense, my main language is C++, and at the time, I hadn't used a lot of python, I got into it because of OpenApollo.

2025. december 14., vasárnap

Log No.2 of the 25/26 Hungarian National Engineering Olympiad

OTIO 2025/26: Almost done with the circuit design


Alright, it has been a bit longer since the last post than I anticipated. In fact it's been so long that I turned 15 in the middle of it lol, but here we go.

First of all, I don't think I disclosed the name of the project in the last post. So, it's called OpenApollo (with blue Nordic Inline font). 

A few changes made since last time, mainly, I didn't like how no available UARTs would've been left on the CM5, so I changed the sensor handling STM from an STM32F042K6T6TR to an STM32F745VET6, which has a 100 pins and a bunch of UARTs, so now along with the previous sensors, it also handles the GPS and the servo current sensing PIC. All this means that the user now has 2 free UART channels to tie directly to the CM5 and probably even more to tie to the sensor STM (I don't remember).

Alongside that, I started working on the GUI for the 'ground station' via the NeutralinoJS framework, which means I unfortunately had to learn HTML and JS. Truly tragic. Seriously, NeutralinoJS is great for this because it uses webview, so it's basically cross-platform by default (and also has JS natively, so using APIs is easier). So far, I've managed to get the FreeStaticMaps API working after wayy too much time. Originally, I tried to use the OpenStreetMaps Maptiles API, but I had issues with zooming in on the maps, because as it turns out, map tiles are like... weird, because they're literally tiles and due to that, zooming kinda goes in a spiral and doesn't put the coordinates I want in the center, so Static maps were what I actually needed. 
Also, why can't RapidAPI just give me code that works by default? Why have you got to hide the source code from me, oh almighty Test Endpoint button? I swear coding this is harder than the circuit design and I refuse to use AI for anything other than explaining async functions.

I did finally decide on the HC-12 as the telemetry method, and it's the boring one, but actually better than the LoRa for multiple reasons. First, unlike the LoRa, its a transciever, so I don't need 2 modules taking up more space than all the other components combined. It can get a pretty good range with just software config, but I also replaced the stock spring antenna (which I'm pretty sure is just an ornament for this tbh) with an optimal 1/4 wavelength antenna, and wow, there is a huge difference. The lowest power mode on this is -1dBm and with the stock antenna, it could barely get through one concrete wall. Now with this antenna and still the lowest power mode, I can literally go 3 stories down and almost into the basement and still mostly get a signal (that's multiple meters of concrete and monopoles aren't even that good at radiating downwards). Originally I though about using what is presumably called a dipole colinear antenna (since its a combination of a dipole and a colinear antenna. duh), but I can't fit 2 antennas into the hole and a colinear array would also be way too large at 433MHz. It's a shame, but this isn't that bad either. The last benefit is that the HC12 is an SDR (I might be really wrong about this, but bear with me), so instead of having to press the pair button on the LoRa, which might not be accessible, you can just select a channel in software and that's it.

I should also revise the code for the PIC, because right now I have this part written 12 times, because I'm not sure how to increment binary numbers via code:

    ADCON0bits.CHS = 0b00000; //Select AN0
    __delay_ms(3); //Delay for channel switching
    ADCON0bits.GO_nDONE = 1; //Start sampling
    while(ADCON0bits.GO_nDONE == 1){ //Wait until sampling is done.
        
    }
    A0result = ((uint16_t)ADRESH << 8) | ADRESL;

But this would be much prettier if made into a function, so I might have to figure that out as not to get struck down by the C gods.

Also, I have a github set up now, so go to github.com/TzEngs if you want to see some absolutely atrocious code (or nothing at the moment).

It's 2:14AM, I'm absolutely done with writing this.

Tamás Zétény Tóth
TTZ

(P.S: I made a logo to put on the circuit board and now I'm gonna start putting it at the end of the posts. I think it's cool. Though I might have to rethink the Bauhaus 93 font)







Log No.6 of the 25/26 Hungarian National Engineering Olympiad

 Damn. A lot longer than I wanted, but I honestly forgot about this blog. So, the PCB arrived in about mid-February, if I'm not mistaken...