diy solar

diy solar

DIY 'Chargenectifier'

Nice python script, but you need to change "true" to "True" in the above example.

I would use asyncio rather than a thread. I use it here for mqtt if you want an example. In that example, there are no messages received on CAN, so CAN doesn't need it, but it's used in receiving mqtt messages.

As for logging, I would just use the standard python logger. This is an example of logging to a file.

Has anyone found a similar charger that goes up to 60V? I'm currently running 17S, and I have a couple of R4850G2s that can almost reach enough voltage, but not quite. They can go over 58V is you want to push them, but I charge at around 59V.

Also, has anyone had CAN communications go out on the R4850G2? The other day one of them stopped talking over CAN, and I noticed that the green LED on the control board doesn't flash anymore, so I'm assuming the CAN bus went out. I haven't checked it under load, but otherwise it seems to power up properly, the fan changes speed, etc, so it seems like it's mostly working.
 
Nice python script, but you need to change "true" to "True" in the above example.

I would use asyncio rather than a thread. I use it here for mqtt if you want an example. In that example, there are no messages received on CAN, so CAN doesn't need it, but it's used in receiving mqtt messages.

As for logging, I would just use the standard python logger. This is an example of logging to a file.

Has anyone found a similar charger that goes up to 60V? I'm currently running 17S, and I have a couple of R4850G2s that can almost reach enough voltage, but not quite. They can go over 58V is you want to push them, but I charge at around 59V.

Also, has anyone had CAN communications go out on the R4850G2? The other day one of them stopped talking over CAN, and I noticed that the green LED on the control board doesn't flash anymore, so I'm assuming the CAN bus went out. I haven't checked it under load, but otherwise it seems to power up properly, the fan changes speed, etc, so it seems like it's mostly working.
Nah it's mostly @upnorthandpersonal work. I was just trying to make it a class so it's easier to manipulate in my (still to be developed) application 🤣 .

For the true -> True part and false -> False part I just fixed it now, thanks (y).

For now only the Docker/Podman image principle works, but it's not doing anything useful yet.

As for the set/get part actually it would require 2 async (of sorts) loops. Still debating whether to implement it in this "master" class or on application level. Also I'm not sure how wise it is to "just let it run", I would probably choose a high enough value for the get() part - say 5 seconds - and implement a "Busy" class property.

Therefore:
- get() is currently requesting data -> self.Busy = True -> set() will need to wait
- set() is currently setting data -> self.Busy = True -> get() will need to wait

And probably this would also require some "loops" of sorts to "retry" when the bus was busy.

Again, I'm pretty sure CAN does some stuff in order to avoid these collisions, but the thing is, if your "set" fails and do nothing about it, it will just fall back to the previous value. You can of course mitigate this, add random delays instead of constant e.g. 1 second, but that still isn't making sure that the message gets delivered.

I could also just rely on the exit code (to be implemented !) in the send_can_message and receive_can_message, so that I retry in the outside loop whenever a fault occurs.

If you have any suggestions on that side as well, feel free. I'm quite new at Python after a very long pause :).
 
You shouldn't get any collisions on the CAN bus, or at least not that you care about. The underlying CAN protocol deals with sensing the bus and retransmitting if an ack is not received. The worst thing you have to worry about, at least on Linux is to not continue sending messages if you're sure the other end is not receiving them, since it can fill up the transmit buffer in the driver.

You should have something that says "I haven't heard from the charger in a while, so I have to assume it's not there anymore", but that could be simply a global (or in a class) entry for the time of the last message received and something that checks it periodically to set a flag that tells the rest of the application that the charger is disconnected.

I don't how this charger works, but the R4850G2 is constantly sending status messages, so it's good enough to just keep reading those and recording the information, and only send messages when you need to update the voltage or current limits, etc.

I haven't used this, but it looks like asyncio is a standard part of python can: https://python-can.readthedocs.io/en/stable/asyncio.html
 
Nice python script, but you need to change "true" to "True" in the above example.

I would use asyncio rather than a thread. I use it here for mqtt if you want an example. In that example, there are no messages received on CAN, so CAN doesn't need it, but it's used in receiving mqtt messages.

As for logging, I would just use the standard python logger. This is an example of logging to a file.

Has anyone found a similar charger that goes up to 60V? I'm currently running 17S, and I have a couple of R4850G2s that can almost reach enough voltage, but not quite. They can go over 58V is you want to push them, but I charge at around 59V.

Also, has anyone had CAN communications go out on the R4850G2? The other day one of them stopped talking over CAN, and I noticed that the green LED on the control board doesn't flash anymore, so I'm assuming the CAN bus went out. I haven't checked it under load, but otherwise it seems to power up properly, the fan changes speed, etc, so it seems like it's mostly working.
Not sure about the insides, but *maybe* you could "trick" the charger by changing a resistor or voltage reference.

I think that's what people do when using a PC ATX power supply as a lab power supply.

But yeah, that would require re-soldering. And NOT sure if it's doable with the Emerson/Vertiv ...
 
You shouldn't get any collisions on the CAN bus, or at least not that you care about. The underlying CAN protocol deals with sensing the bus and retransmitting if an ack is not received. The worst thing you have to worry about, at least on Linux is to not continue sending messages if you're sure the other end is not receiving them, since it can fill up the transmit buffer in the driver.

You should have something that says "I haven't heard from the charger in a while, so I have to assume it's not there anymore", but that could be simply a global (or in a class) entry for the time of the last message received and something that checks it periodically to set a flag that tells the rest of the application that the charger is disconnected.

I don't how this charger works, but the R4850G2 is constantly sending status messages, so it's good enough to just keep reading those and recording the information, and only send messages when you need to update the voltage or current limits, etc.

I haven't used this, but it looks like asyncio is a standard part of python can: https://python-can.readthedocs.io/en/stable/asyncio.html
Well I can tell you that in the past (with a POWERED USB HUB) combined in a bug in the previous version of the code (NOT properly terminating CAN commands & closing the CAN bus), I was left in a status that, even after a reboot, I could send ANY CAN command, everything would succeed (from the adapter point of view). But of course, nothing was being sent to the Charger itself ! Not even errors in transmission !

EDIT: So probably checking the status of the internal variables and, if say within 30 seconds they don't change AT ALL, then mark that something weird happened.
 
Well I can tell you that in the past (with a POWERED USB HUB) combined in a bug in the previous version of the code (NOT properly terminating CAN commands & closing the CAN bus), I was left in a status that, even after a reboot, I could send ANY CAN command, everything would succeed (from the adapter point of view). But of course, nothing was being sent to the Charger itself ! Not even errors in transmission !
What CAN interface are you using? I had similar issues with a raspberry pi hat, so stopped using it, but have not had any issues with Canable-based USB adapters.

Actually, a powered USB hub might be an issue if the USB adapter doesn't get properly reset on reboot. Like I said, I think the biggest problem is packets getting stuck in the send buffer. If the adapter doesn't get reset it could be stuck with packets buffered that it's trying to send.
 
What CAN interface are you using? I had similar issues with a raspberry pi hat, so stopped using it, but have not had any issues with Canable-based USB adapters.

Actually, a powered USB hub might be an issue if the USB adapter doesn't get properly reset on reboot. Like I said, I think the biggest problem is packets getting stuck in the send buffer. If the adapter doesn't get reset it could be stuck with packets buffered that it's trying to send.
That's what I observed: https://github.com/luckylinux/solar-controller/?tab=readme-ov-file#usb-hubs--experienced-issues

I think the CAN Adapter couldn't "Power Cycle" on Raspberry PI Reboot.

As soon as I removed the power from the "Powered" USB Hub and rebooted the Raspberry Pi, the thing started working again.

I'm using this Canable Pro (with Galvanic Insulation) adapter: https://www.aliexpress.com/item/1005004972158302.html
 
Probably the easiest would therefore be: storing the timestamp (I usually prefer UNIX timestamp in seconds, easier to handle until 32 bit overflow at least ...) whenever a new (different) and plausible value is received.

Eg. I would modify this
Python:
# CAN receiver listener (register values within an object)
    # Not really the best approach, but how to pass optional argument "echo" to CAN.Notifier in case of another solution ?
    def can_listener_store(msg):
        # Is it a response to our request
        if msg.data[0] == 0x41:
            # Convert value to float (it's the same for all)
            val = struct.unpack('>f', msg.data[4:8])[0]

            # Check what data it is
            match msg.data[3] :
                case 0x01:
                    self.Readout.Output_Voltage = val
                case 0x02:
                    self.Readout.Output_Current_Value = val
                case 0x03:
                    self.Readout.Output_Current_Limit = val
                case 0x04:
                    self.Readout.Temperature = val
                case 0x05:
                    self.Readout.Input_Voltage = val

with this (pseudocode for the "and"s)
Python:
# CAN receiver listener (register values within an object)
    # Not really the best approach, but how to pass optional argument "echo" to CAN.Notifier in case of another solution ?
    def can_listener_store(msg):
        unixtime = time.time()

        # Is it a response to our request
        if msg.data[0] == 0x41:
            # Convert value to float (it's the same for all)
            val = struct.unpack('>f', msg.data[4:8])[0]

            # Check what data it is
            match msg.data[3] :
                case 0x01:
                    if val >0 and val <60 and val ~= self.Readout.Output_Voltage:
                           self.Received_Timestamps.Output_Voltage = unixtime
                    self.Readout.Output_Voltage = val

                case 0x02:
                    if val >=0 and val <65 and val ~= self.Readout.Output_Current_Value:
                           self.Received_Timestamps.Output_Current_Value = unixtime
                    self.Readout.Output_Current_Value = val
                        
                case 0x03:
                     if val >=0 and val <62.5 and val ~= self.Readout.Output_Current_Limit:
                           self.Received_Timestamps.Output_Current_Limit = unixtime
                    self.Readout.Output_Current_Limit = val

                case 0x04:
                     if val >=-40 and val <60 and val ~= self.Readout.Temperature:
                           self.Received_Timestamps.Temperature = unixtime
                    self.Readout.Temperature = val

                case 0x05:
                    if val >=0 and val <300 and val ~= self.Readout.Input_Voltage:
                           self.Received_Timestamps.Input_Voltage = unixtime
                    self.Readout.Input_Voltage = val

But of course you cannot do that with the "sends".
We might only be able to do a cross-check for "Output_Current_Limit" after a set has been applied and a minute or so has elapsed, but it's not going to be fully "in sync".
 
Last edited:
@upnorthandpersonal I did again some changes, all NOT tested so far. I need to motivate myself and solder 2 damn wire on a PCB to test the code on a non-production system. Anyway, in order to avoid the situation I was having before (USB powered hub + bug in the CAN connection not being properly closed) I implemented 2 basic checks to evaluate if data is "reasonable":
- Read Data changes after a while (there are always tolerances, small variations, measurement noise, etc)
- Read Data is "plausible"


So I had to rename your set functions to __set, as the user will store these settings in the object itself, which will in turn take care of all the looping (read and write), checks, etc.

At least that's the plan.

But, unless I miss something, there is NOT an easy way to provide both function-like access like your script (which I liked very much in the early days thanks to the __main__ and BASH scripting possibility) with what I do OOP. Both inheratance, includes, etc have downsides. Maybe we just have to bite the bullet and "split". In the future I won't personally need the interactive mode, but as you saw, it can be easier (in some circumstances at least).

Feel free to throw in your 0.05$ if you have better implementation ideas. I still need to cleanup the code and there are probably 20+ bugs right now which I first need to iron out 🤣
 
@upnorthandpersonal I setup a small test bench at my desk (noisy). Of course no load, just 230 VAC and CAN communication.

Your script works, but I am wondering if the "default" current limit is correct (I think you said "absolute value in amps").

Code:
Temp (C) : 14.107421875
Vin (VAC) : 220.623046875
Vout (VDC) : 51.0390625
Iout (IDC) : 0.0
Output Current Limit : 0.96875

Doesn't this more look like a PU value ?
 
Maybe we just have to bite the bullet and "split".

Yeah, I wasn't planning on expanding that as far as you are going with it. I even made a slimmed down version (pushed just now) only reading the Rectifier for integration with Grafana (like I did for the JK BMS, Multiplus, MUST stuff on my Github).
 
Current limit is in %, and I think it's 0% - 100%. It's not documented, so call it a guess.
And by default it's set to 0.96% ? I'd rather expect 96% ...

Well ... "default". I cloned your git repo since I'm actively working on mine and wonted something stable to kickstart.

Current is set to 50A in the example code we shared.

50A set / 62.5A rated * 121% = 96.8%.

So it is in p.u. in my view, not in percentage (unless you multiply it by 100 of course).

Bash:
#!/bin/bash

# Activate venv
source venv/bin/activate

# Define adapters
adapters=()
adapters+=("can-grid-00")
#adapters+=("can1")

# Reconfigure all adapters
for adapter in "${adapters[@]}"
do
    echo "Re-initialize and re-configure adapter <$adapter>"
    python ./rectifier.py --interface "$adapter" -C
done

# Loop
while true
do
    for adapter in "${adapters[@]}"
    do
        # Set Parameters
        python ./rectifier.py --mode "set" --interface "$adapter" --voltage 51.0 --current_value 50
        sleep 10

        # Read Data
        python ./rectifier.py --mode "get" --interface "$adapter"
    done
done
 
Yeah, I wasn't planning on expanding that as far as you are going with it. I even made a slimmed down version (pushed just now) only reading the Rectifier for integration with Grafana (like I did for the JK BMS, Multiplus, MUST stuff on my Github).
No fair enough (y). I also do not know where I want to end exactly 🤣.

My plan was to make it "self sufficient" and "self contained" in a class which does all the required looping and set/get.

Then the user class would do set/get every 10 seconds / 1 minute / 5 hours / doesn't matter. The class / object would still make sure to read and write everything that was previously set.

But I am for sure very proud I got it to work with Podman (and probably Docker) Rootless :).
 
Current limit is in %, and I think it's 0% - 100%. It's not documented, so call it a guess.
I would expect up to 121% for the full rating actually (62.5A) so you "close the loop" so to speak.

Look at my "reverse engineer" of the 50A example ending up at 96%...

62.5A would be sure be higher
 
Like I said, it's undocumented, but you will get a higher current at lower voltage for the same power output, so maybe that's impacted?
That's true in principle and also reflected in the datasheet.
1709655599968.png

You cannot exceed the "nominal" current of 62.5A for sure.

I still think the current limit in % or p.u. is always reflecting against the "nominal" 62.5A current, NOT the maximum it could go at the current voltage.

But we could run a test and find out :) . Maybe depending on the set voltage it would change. I have voltage set at 51V and current limit set at 50A and am getting 96%. 96.8% would be the correct value if it always referred to the "nominal" current.
 
Back
Top