• Hello ES! We could use some help to get us past the finish line on building the new knowledgebase for the forum.
    Can you donate? Please see our fundraising page. Thank you!

Understanding and Using EGO Power+ Batteries

I recorded the signal with an oscilloscope and analyzed it in Excel. I have not written any code for this.
@mart498 do you have the excel file that you can share or the specific timing slot interval the Ego power+ system uses?

Has anyone else tried to mock/mimic their protocol? I can see it’s been done by all the knockoff batteries.
 
I have reverse engineered some messages between my quick charger and 7.5 Ah battery. Used protocol is similar to 1-wire but bit timing is different.

Here is one message from the charger:
View attachment 273691

CRC calculation method is the same as used for 1-wire:
width=8 poly=0x31 init=0x00 refin=true refout=true xorout=0x00 check=0xa1 residue=0x00

The full set of packets when full battery is inserted to charger is the following:
View attachment 273692

Each messages (except the header messages) consist 72 bits (6 bytes command/query name + 2 data byte + 1 CRC byte). For GET_VM query the battery is answering:
"OUT_VM" + 0x2E + 0xE0. Converting it to 0xE02E is in decimal 57390 mV = 57.390 V.

I haven't had time to decode GET_CM (current?) and GETCG1 (charge level?) value bytes as it would need testing it on different charge levels.
Continuing on the work of mart498, I have captured and partially decoded the communication between the battery and charger. Attached are a few csv's with ~20sec captures. These are sampled at 100kS/s, low pass filtered, and quantized to make the files much more compressible without losing the content.

1776461023046.png
 

Attachments

  • ego wfm data.7z
    64.9 KB · Views: 1
Continuing on the work of mart498, I have captured and partially decoded the communication between the battery and charger. Attached are a few csv's with ~20sec captures. These are sampled at 100kS/s, low pass filtered, and quantized to make the files much more compressible without losing the content.
Haha, what a timing! I've been working on something too!

I've been focusing on communication between battery and tools. My goal is to use an Ego battery on my ebike but having capabilities to monitor temp AND per-cell voltage.

I know for a fact that the tools don't only have a low voltage cutoff. My mower will cut if one of the cell group is low enough even if the total pack voltage isn't below the threshold.

What are you using for capturing? I'm using a cheap logic analyser hooked up using the ABH3000 + ADB1000 (opened up)

1776572769476.png

I tested with a Nexus 400W inverter (lots of data) + a LB5800 leaf blower (couldn't get anywhere). The Nexus 400 can also charge but it's past midnight and I need some sleep !

So far I've found (meaning are using claude for help)

CommandDirectionQuery dataResponse dataMeaning
START_Tool → Battery0x0000Session start (part of handshake pair)
ADJUSTBattery → Tool0x0000Handshake ack / init
RD_SPCTool → Battery0x00000x0E0NPack SKU ID (14S, model N)
RD_CAPTool → Battery0x0000uint16Per-cell Ah × 100 (e.g. 250 = 2.5Ah cells)
RD_FSHTool → Battery0x38000x38XXStatus/fault flags (Gen2 only; Gen1 stubs to 0x38FF)
RD_TMPTool → Battery0x000Nuint16Temperature sensor N (N=0 primary)
RD_VOLTool → Battery0x000Nuint16Cell N voltage in 10 mV units

RD_VOL is what I'm interested in.. and there seems to be only 7 value returned... so it's using pairs??? that would be weird.. I feel like I'm missing something.. but it's time for bed!

EDIT : all good, RD_VOL works for all 14 cell groups.. but for some reason, the reply is SUPER quick on the first 7 (2.4ms) and significantly longer on last 7 (30ms). Might be an artifact of my logic analyser but I'll build a small arduino sketch to actually send messages to the battery instead of just sniffing
 
Last edited:
I have used both a scope and fpga for capturing data. Both work fine. The scope is convenient because it's easy to tell which direction the communication is going. The voltage swing is slightly different when the battery is transmitting vs. the charger.

My goal is the opposite. I have built a backpack battery (14s10p, ~31Ah) and want to use it with my ego tools and chargers. You're table above is very helpful. It'll be a couple weeks before I get back to this project but I'll post any results I get.

I can also confirm your observation about the individual cell voltages. I have two ego packs that have failed with one bad cell group. The tool/charger disables even though the total string voltage is normal. One pack was recovered by manually balancing, the other was too far gone.
 

Attachments

  • IMG_5023.jpeg
    IMG_5023.jpeg
    241.6 KB · Views: 9
  • IMG_5261.jpeg
    IMG_5261.jpeg
    228.8 KB · Views: 9
  • IMG_6410.jpeg
    IMG_6410.jpeg
    142.7 KB · Views: 7
  • IMG_6572.jpeg
    IMG_6572.jpeg
    178.9 KB · Views: 8
  • IMG_6869.jpeg
    IMG_6869.jpeg
    65.2 KB · Views: 9
  • IMG_7624.jpeg
    IMG_7624.jpeg
    185.6 KB · Views: 8
Last edited:
I can also confirm your observation about the individual cell voltages. I have two ego packs that have failed with one bad cell group. The tool/charger disables even though the total string voltage is normal. One pack was recovered by manually balancing, the other was too far gone.
Interesting! Do you know which message is being sent so that the tool refuses to work?

When I logged the communication between my leaf blower and the battery, all I got was what seemed like a heart beat sent by the tool every second or so to keep the battery alive.

I just picked up a cheap scope to understand some weird behavior I've seen - I figured I could decode who's talking to who! Was thinking of hooking up 2 channels (battery vs tool) so I could perhaps use the timing.

When you say the voltage swing is different? how? Also I assume this is 3.3V? or something like that?
That software looks nice, I'm assuming you built something custom? I was planning to publish my results on GitHub at one point. Maybe we could collaborate?
 
I have not scoped the battery to tool communication yet, only the battery to charger. What I see is Voh is around 4v and Vol either 0v or ~0.6v, depending on the direction. There's one or more shorter packets (~40bits) sent at the start of charge, followed by the call and response pattern described earlier.

1776750389384.png

I did create a simple application to cycle and capacity test cells before assembly. I was surprised to find 99% of the cells I pulled from scrapped ebike packs still met the original MJ1 data sheet specs. Certainly happy to share any info I find, I just won't be very active on this for awhile.
 
So I ordered a cheap LHT00SU1 logic analyser with an analog input off AliExpress. Also coded (using claude) a super crap Pulseview Decoder for the signal.
What I can gather :
-Tool pulls up pin to around 4.3V
-Tool then cycle between 4.3V and 0V to send messages to the Battery
-Battery then cycle between 4.3V and 0.8V when responding

Here's an example of a RD_VOL query and response

1777919374760.png

Now I need to build a small circuit to try to replicate this behavior. Fun times ahead!
 
Alright, I'm not 100% sure of the circuitry (In fact I know I need to use a voltage divider, not a resistance) - need to scope it to make sure it's safe... but by using a pullup 470ohms resistor on a 5V feed on an arduino, I can then use a digital pin to drain it (pull to 0) to send bytes.

Behavior of Gen 1 and Gen 2 (fuel gauge) is a bit weird which explains the odd flashing light when plugged in a Nexus inverter - my code does the same.

  1. First, when pulling up to around 4V, the battery lights up (steady) and sends a few "START_" messages (with a delay between each send)
  2. Then if I respond with a "ADJUST" message, Gen 1 Batteries flash all sort of colors and after a while send me another "START_". I can resend Adjust and it just loops like that. In between those, I can communicate with the battery but it's erractic (like the Nexus does)
  3. On Gen 2 batteries, the Adjust actually puts the battery in "run mode" with the fuelgauge blinking !
  4. Then, after that exchange (Start_ + Adjust) I can send commands : RD_SPC, RD_CAP, RD_FSH, RD_TMP & RD_VOL
  5. RD_SPC : seems to be the type of battery. like a sku? unsure
  6. RD_CAP : capacity, in Ah of a single cell - I've seen 250 (Gen 1 5Ah and 7.5Ah & Gen2 10Ah) and 300 (Gen 2 12Ah)
  7. RD_FSH : I think it's a fault code, need to drain a battery to test.
  8. RD_TMP : seems to be temperature in F. Put a battery 1-2 hours in my fridge and it seemed to match
  9. RD_VOL : you send which cell you want, and you get the value in 10mV increment
Attached the Arduino code to this message (renamed the .ino file to .cpp so the website let it attach)
See message below, publishing on Github under GPL
Output of my ugly Arduino code (used Claude) :

Code:
21:47:53.504 -> EGO battery decoder - listening on pin 2
21:47:53.504 -> Auto-ADJUST on START_ is ON. Keys: v r t s c f a x h
21:47:53.504 -> Initial line state: HIGH
21:48:42.859 -> (long LOW 49418192us)
21:48:42.913 -> (long LOW 11076us)
21:48:42.913 -> (long LOW 3188us)
21:48:42.913 -> RX  [1] @51437ms  4b  (+4b)
21:48:43.143 -> RX  [2] @51643ms  40b  AA 0B 4A 48 8B  CRC ok  [ID]
21:48:43.301 -> RX  [3] @51823ms  40b  AA 0B 4A 48 8B  CRC ok  [ID]
21:48:43.518 -> RX  [4] @52002ms  73b  00 00 5F 54 52 41 54 53 B0  CRC ok  cmd="START_" data=0x0000 (0)
21:48:43.518 ->     -> auto-ADJUST
21:48:43.518 -> TX  @52071ms  9b  00 00 54 53 55 4A 44 41 93  cmd="ADJUST" data=0x0000
21:48:43.702 -> RX  [5] @52202ms  73b  A0 00 54 53 55 4A 44 41 A9  CRC ok  cmd="ADJUST" data=0x00A0 (160)
21:49:00.313 -> Keys: v=RD_VOL(cell0) r=sweep-all-cells t=RD_TMP s=RD_SPC c=RD_CAP f=RD_FSH a=ADJUST x=toggle-auto-ADJUST
21:49:06.146 -> TX  @74692ms  9b  00 00 50 4D 54 5F 44 52 1E  cmd="RD_TMP" data=0x0000
21:49:06.271 -> RX  [6] @74756ms  73b  48 00 50 4D 54 5F 44 52 7D  CRC ok  cmd="RD_TMP" data=0x0048 (72)  T=72F?
21:49:19.340 -> TX  @87886ms  9b  00 00 43 50 53 5F 44 52 0A  cmd="RD_SPC" data=0x0000
21:49:19.466 -> RX  [7] @87951ms  73b  03 0E 43 50 53 5F 44 52 B0  CRC ok  cmd="RD_SPC" data=0x0E03 (3587)  S=14S model 3
21:49:28.105 -> TX  @96659ms  9b  00 00 50 41 43 5F 44 52 81  cmd="RD_CAP" data=0x0000
21:49:28.195 -> RX  [8] @96723ms  73b  FA 00 50 41 43 5F 44 52 0A  CRC ok  cmd="RD_CAP" data=0x00FA (250)  Ah/cell=2.50
21:49:49.837 ->     -> sweep cells 0..13
21:49:49.837 -> TX  @118416ms  9b  00 00 4C 4F 56 5F 44 52 BC  cmd="RD_VOL" data=0x0000
21:49:49.962 -> RX  [9] @118480ms  73b  6E 01 4C 4F 56 5F 44 52 D9  CRC ok  cmd="RD_VOL" data=0x016E (366)  V=3.66
21:49:49.994 -> TX  @118578ms  9b  00 01 4C 4F 56 5F 44 52 81  cmd="RD_VOL" data=0x0100
21:49:50.119 -> RX  [10] @118642ms  73b  6F 01 4C 4F 56 5F 44 52 9A  CRC ok  cmd="RD_VOL" data=0x016F (367)  V=3.67
21:49:50.166 -> TX  @118739ms  9b  00 02 4C 4F 56 5F 44 52 C6  cmd="RD_VOL" data=0x0200
21:49:50.292 -> RX  [11] @118803ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
21:49:50.292 -> TX  @118901ms  9b  00 03 4C 4F 56 5F 44 52 FB  cmd="RD_VOL" data=0x0300
21:49:50.456 -> RX  [12] @118965ms  73b  6D 01 4C 4F 56 5F 44 52 1C  CRC ok  cmd="RD_VOL" data=0x016D (365)  V=3.65
21:49:50.456 -> TX  @119063ms  9b  00 04 4C 4F 56 5F 44 52 48  cmd="RD_VOL" data=0x0400
21:49:50.587 -> RX  [13] @119127ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
21:49:50.620 -> TX  @119224ms  9b  00 05 4C 4F 56 5F 44 52 75  cmd="RD_VOL" data=0x0500
21:49:50.747 -> RX  [14] @119288ms  73b  6E 01 4C 4F 56 5F 44 52 D9  CRC ok  cmd="RD_VOL" data=0x016E (366)  V=3.66
21:49:50.780 -> TX  @119385ms  9b  00 06 4C 4F 56 5F 44 52 32  cmd="RD_VOL" data=0x0600
21:49:50.907 -> RX  [15] @119449ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
21:49:50.940 -> TX  @119546ms  9b  00 07 4C 4F 56 5F 44 52 0F  cmd="RD_VOL" data=0x0700
21:49:51.095 -> RX  [16] @119638ms  73b  6E 01 4C 4F 56 5F 44 52 D9  CRC ok  cmd="RD_VOL" data=0x016E (366)  V=3.66
21:49:51.133 -> TX  @119708ms  9b  00 08 4C 4F 56 5F 44 52 4D  cmd="RD_VOL" data=0x0800
21:49:51.259 -> RX  [17] @119802ms  73b  6F 01 4C 4F 56 5F 44 52 9A  CRC ok  cmd="RD_VOL" data=0x016F (367)  V=3.67
21:49:51.305 -> TX  @119873ms  9b  00 09 4C 4F 56 5F 44 52 70  cmd="RD_VOL" data=0x0900
21:49:51.423 -> RX  [18] @119967ms  73b  6F 01 4C 4F 56 5F 44 52 9A  CRC ok  cmd="RD_VOL" data=0x016F (367)  V=3.67
21:49:51.461 -> TX  @120037ms  9b  00 0A 4C 4F 56 5F 44 52 37  cmd="RD_VOL" data=0x0A00
21:49:51.591 -> RX  [19] @120132ms  73b  6F 01 4C 4F 56 5F 44 52 9A  CRC ok  cmd="RD_VOL" data=0x016F (367)  V=3.67
21:49:51.632 -> TX  @120201ms  9b  00 0B 4C 4F 56 5F 44 52 0A  cmd="RD_VOL" data=0x0B00
21:49:51.759 -> RX  [20] @120296ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
21:49:51.802 -> TX  @120366ms  9b  00 0C 4C 4F 56 5F 44 52 B9  cmd="RD_VOL" data=0x0C00
21:49:51.959 -> RX  [21] @120460ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
21:49:51.959 -> TX  @120531ms  9b  00 0D 4C 4F 56 5F 44 52 84  cmd="RD_VOL" data=0x0D00
21:49:52.086 -> RX  [22] @120625ms  73b  70 01 4C 4F 56 5F 44 52 34  CRC ok  cmd="RD_VOL" data=0x0170 (368)  V=3.68
 
Last edited:
Decided to scope it. Yup, I'm running the circuit hot.

1777948432900.png

4.71V high
0.31V low
1.80V low when the battery pulls it down

I need to fix this but now.. bed time!
 
Ok, big update. I think I have a pretty good picture of the messages during charging / discharging on a Nexus 400 / 3000 (they go through voltage per cell and so on)... And charging on a regular charger (what mart498 saw).

Decided to switch from ArduinoIDE to PlatformIO - works better (VS Code). Also decided to share my full code under GPL - see the 2 repos here :
The firmware emits NDJSON over USB so the GUI just consumes structured events — live discharge / charging / idle panels with auto mode-switching, capture-to-file, and the firmware can also stand in as a tool and send commands.



New stuff worth flagging:
  • Charging is interesting. The BMS sends commands to raise or decrease the current. At one point, the charger reports the battery full (stops blinking) - battery also stops blinking and we get a "charge done" message... but the charge still sends a few amps to top the battery off. Then when the battery is actually full full, it shuts down the charging. Oddly enough, if you unplug and replug the battery during this, it never start the trickle charge again. oh well!
  • I'm using the analog pin on the Arduino to read from the bus voltage. Tool drives LOW to ~0 V, battery to ~0.7 V. One ADC read at frame start tells you reliably who sent (tool vs battery). This isn't 100% reliable because some tools don't pull to 4.3V, my leaf blower for example, pulls to 3.9... so I'm using a constant to decide which one does what.
Full command tables and protocol-flow diagrams are in the firmware README.

Hardware is dead simple:

  • Sniffing only: Mega GND to battery -, Pin 2 + A0 directly on the D pin, no 5 V feed.
  • TX-capable: 5 V → 1N4148 → 100 Ω → D pin. The diode pins idle HIGH at ~4.3 V matching the bus's natural level and blocks reverse current back into the Mega.
Screenshots:
Nexus 400 Discharging a battery :
5vWF8dH3Tn.png

550W "Rapid Charger" charging a battery
a6AC4SKAGa.png

210W "Charger" charging a battery

GnX2JAgL0G.png

Lots of help from claude on this obviously. I'm a dev but this makes me MUCH more productive.

Happy to get anyone's contribution. Right now I'm using an Arduino Mega 2560 R3 because I had it lying around and it supports 5V logic out of the box. I also have a Uno somewhere but I might want to use something smaller to create a small box to sniff what happens when my mower cuts out even if the battery itself is not at the low-voltage threshold.
 
oh, and I totally forgot. I pimped my sniffing setup.

was tired of pins pulling out when cats jumped on my desk.

So I used "posi taps" to do a clean install in the belt adapter fake battery :

1778282171504.png

Then I added an M8 connector

1778282192788.png


Makes it super clean

1778282207730.png

Now I just need to do something for the other end. Having a breadboard isn't super convenient. I might just use another M8 connector or perhaps a simple TRS jack

This is on my readme on my github page but :

Nexus 400 stuff :
CommandWire bytes (rev)DirectionNotes
START__TRATSbatt → toolSession invite. Data 0x0000. Sent after the ID×2 heartbeat (and skipped on the first Gen1 connection — see Protocol flow).
ADJUSTTSUJDAtool → batt and batt → toolSession handshake. Tool always sends 0x0000. Battery's reply differs by generation: Gen1 echoes 0x0000, Gen2 replies 0x00A0. The 0x00A0 reply is the cleanest Gen2 marker we have.
RD_VOLLOV_DRtool ⇄ battTool: byte[1]=cell_idx (0..13), byte[0]=0x00. Batt: voltage in centivolts (v/100 V).
RD_TMPPMT_DRtool ⇄ battTool: byte[1]=sensor_idx. Batt: temperature, units appear to be °F.
RD_SPCCPS_DRtool ⇄ battTool: 0x0000. Batt: byte[1]=S-count, byte[0]=model. model=1 on Gen1, model=3 on Gen2 (one-sample evidence — may be a generation marker).
RD_CAPPAC_DRtool ⇄ battTool: 0x0000. Batt: Ah/cell × 100.
RD_FSHHSF_DRtool ⇄ battTool: Q=0x3800. Batt: status; byte[0]=0xFF = Gen1 stub, otherwise Gen2 status bits.

Charging stuff :
CommandWire bytes (rev)DirectionNotes
START__TRATSbatt → tool and tool → battBattery sends 0x0000 (same as diagnostic). Charger replies with its own START_ payload that varies per charger model — observed 0x00D7 (215) on a 3 A charger, 0x000A (10) on an 8 A charger. Likely a model / capability code.
EVACHGGHCAVEboth"Enter charge mode" (electric-vehicle-adjust-charge?). data=0x0403 (1027) on an active charge; data=0x0000 on a full-battery session. Looks like a mode/state marker.
GETCG1 / OUTCG11GCTEG / 1GCTUObatt / tool"Charge stage 1" query/reply. Active-charge session: both 0x0000. Full-battery session: charger replies 0x1000 (4096). Possibly a state flag.
GET_VM / OUT_VMMV_TEG / MV_TUObatt / toolBattery asks the charger's output voltage. OUT_VM data is centivolts (e.g. 0x1770 = 6000 → 60.00 V). On a full-battery session this is the charger's rated voltage, not a live setpoint.
GET_CM / OUT_CMMC_TEG / MC_TUObatt / toolBattery asks the charger's output current. OUT_CM data is centiamps (e.g. 0x012C = 300 → 3.00 A; 0x0320 = 800 → 8.00 A). On a full-battery session this is the charger's rated capability, not an ongoing setpoint.
EN_OUTTUO_NEtool → battCharger toggles its output: data=0x0001 enables, 0x0000 disables. Skipped on a full-battery session — see DISOUT.
DISOUTTUOSIDbothDisable output. Sent in the full-battery end-of-session sequence in place of EN_OUT. Battery sends 0x0000, charger acks with 0x0005 (5; meaning unclear — possibly seconds).
CHGINGGNIGHCboth"Charging" heartbeat / handshake. Data 0x0000 both sides.
CHGFULLUFGHCboth"Charge full." BMS signals the battery is functionally ready to usenot "charging is done". Observed firing mid-session during a real charge cycle: ~70 s of OUTCUR taper continued after CHGFUL (194 → 164 cA), driven by DECCUR>=0 from the BMS. Also used in the already-full plug-in sequence (where CHGFUL fires within ~3 s with no charging in between). Both sides exchange data=0x0000.
ADDCURRUCDDAbatt → toolBattery requests an increment to charge current (delta in unknown unit; observed values 2 and 5). Drives the closed-loop ramp up. Dominant during early charging; stops being sent once the BMS is into the taper phase. Not seen at all on an already-full session.
DECCURRUCCEDbatt → toolBattery requests a decrement to charge current — counterpart to ADDCUR, dominant during the taper / top-off phase. Observed values 0..3 (data=0 most common). Charger doesn't echo DECCUR; it just lowers its OUTCUR in response.
OUTCURRUCTUOtool → battCharger reports actual current it's delivering. Climbs monotonically while battery sends ADDCUR>0; tapers monotonically while battery sends DECCUR>=0.
FAN_ONNO_NAFbothFan control during active charging. Battery sends a request (always 0x0064 = 100), charger replies with the actual setting (ramps 5 → 14 → 20 → 28 → … → 139). Units uncertain — looks like a PWM duty / set-point.
FANOFFFFONAFbothFan off. Sent in the full-battery end-of-session sequence in place of FAN_ON (no thermal load to dissipate). Both sides exchange data=0x0000.
CHGPCTTCPGHCbothState-of-charge percentage. Battery reports actual SOC (e.g. 28 → 29 → 30 % during the capture). Charger always replies 0x0001 — likely an "ack" rather than its own value.
SLEEP__PEELSbothSession terminator. Sent at the end of a full-battery session; bus goes idle afterwards. Both sides exchange data=0x0000.
 
This looks great! I worked with the EGO protocol previously, intercepting messages between the leaf blower and the battery (boy, was testing noisy!). The individual cell voltages was what I was after, but I didn't have the commands then.

I'm assuming after charging it attempts to balance the cells? Odd that it won't balance them if you put the battery back on the charger (unless the cells were already balanced by then?)
 
Quick update on the RD_SPC.

The response is actually the number of cells in Series and in Parallel. I tested with those packs :
  1. Gen 1 5Ah
  2. Gen 1 7.5Ah
  3. 2xGen 2 10Ah
  4. 2xGen 2 12Ah

For instance :
03 0E 43 50 53 5F 44 52 B0
Decodes to :
ByteValueMeaning
00x030-indexed Cells in Parallel so in that case 4P
10x0ES count : 14s
20x43C
30x50P
40x53S
50x5F_
60x44D
70x52R
80xB0CRC

RD_SPC - 14s4p

With the RD_CAP of either 250 or 300, it matches perfectly with the label on the battery. A 10Ah for instance is a 14s4p with each cell at 2.5Ah

The 12Ah is similar but with 3Ah cells (exact same RD_SPC message) but with a 300 RD_CAP.

So :
03 0E 43 50 53 5F 44 52 B0 -> RD_SPC 14s4p
FA 00 50 41 43 5F 44 52 0A -> RD_CAP 2.5Ah
10Ah pack

and
03 0E 43 50 53 5F 44 52 B0 -> RD_SPC 14s4p
2C 01 50 41 43 5F 44 52 2B -> RD_CAP 3.0Ah
12Ah pack


Funny enough when the BT/WiFi was still working on my Nexus 3000 inverter (they dropped the app), it would recognize pack models but would list the 12Ah packs as "10Ah" so I guess they had a list of available packs and went with it ?

Updated both projects on github to account for that - also switched the UI to tabs.
The ID that is sent by the battery over and over again seems unique to each battery but I didn't find a correlation between it and the actual serial number.

Also added a "keep awake" (works on windows at least) so it prevents the computer from going to sleep if connected

1778708586573.png
 
Alright - I played with my Nexus 3000 power station a bit yesterday. For those who don't know, this thing used to be able to connect via WiFi and BTLE to a "Ego Power+" app. You could see the load in Watts, turn on / off individual 120V outlets and monitor time left for discharging / charging. It would use BTLE to configure the WiFi connection that would then stick. It would also show you state of charge of the batteries and I noticed back then that some of my old batteries would stop charging and show less that 100%.

When I say "used to be able" is because Ego dropped support for the app. It was pulled from the App Store. If you still had the app on your phone, it wouldn't connect (it has a login screen at first)... so even if you don't care about WiFi, BTLE wouldn't even be possible because the login screen prevents you from getting to the controls.

I was curious about how the Nexus shows degradation so I decided to try to reverse engineer the protocol between the Nexus and the app.

I noticed that it was still showing the WiFi icon on the UI so I did a TCP dump on the accesspoint and it looks like it's trying to connect to a static IP on AWS over port 9000. This IP is dead now. Decided to do a Destination NAT to redirect that to a small Linux box I have running "nc" and got some packets... but they looked encrypted :(. Did a few tests and the key seems to change so it was a dead end. The device hostname is "espressif"... which is the default name of a ESP device, probably a ESP8266...Figured I could dump the firmware's flash if they didn't use secureboot w/flash encryption (which was likely since they didn't bother to change the name of the device). This required me to open the unit up and so one... which I didn't feel like doing. So I turned to BT LE

Poked around on my phone using nRF connect and found the device with some suspiciously familiar.

1778770266760.png

Looks like UART over BLE on a ESP32!

When connecting to it, the setting button on the unit would flash blue for a few seconds and I would be able to poke stuff but would get disconnected. Figured there was probably a secret handshake going on. My first instinct was to use the phone's dev setting to snoop on the BTLE traffic but I wasn't able to go past the login screen on the latest version (2.2.3). Found an old version of the APP (1.1.0) which was listed as "now support control of nexus station" in the release notes. Loaded that on Android emulator (not installing random APK from the web on my real phone lol) but it was still blocked on login... CRAP.

Then it became super obvious : I could just decompile the bytecode from the apps and try to see if I would be able to figure it out.

Decompiled version 1.1.0. Unsurprisingly the Java code was obfuscated but Claude Code took less than 10min to figure out the handshake. Then built a small python based CLI to interact with the Nexus and it worked! The 1.1.0 app was very limited it seems though - I could turn off and on outlets and get some stats but almost none of what I remembered from the later versions.

So I decompiled version 2.2.3. Surprise Surprise, it's a Xamarin based web app. Found the actual HTML for the Nexus and it's exactly how I remembered it. So I extracted the .NET DLLs (you read that right) from the .so file then use a C# decompiler to get the source code. This one wasn't obfuscated. Snooped around then launched Claude code on it too. It confirmed a few unknowns we had before + how to fully decode the protocol.

Here's the output of the Nexus with 1x5Ah (old and very used), 1x7Ah(also old and very used) + 1x10Ah (2yo, used infrequently in my snow blower)


Code:
09:16:51 INFO    nexus.client :: connecting to [REDACTED]
Nexus PSID: [REDACTED]
  State:  charging  (WiFi=on, BT=on, low_power=False)
  Output: 0W / 0W (0s to full)
  Clock:  2026-03-21 12:06:00
  Outlets:
    AC1: off      0W
    AC2: off      0W
    AC3: off      0W
    USB: off
  Batteries:
    B1: loaded       86%   4.3Ah/5.0Ah   56.69V   0.00A  T=  0
        ID: 00000000000023D02A06
    B2: loaded      100%   9.9Ah/10.0Ah   58.13V   0.00A  T=  0
        ID: 000000000000AB592369
    B3: loaded       77%   5.7Ah/7.5Ah   55.12V   0.00A  T=  0
        ID: 00000000000023F82A79
    B4: (not connected)

This is after a night of charging and sitting idle with the screenshowing "Charging complete"

As you can see the Nexus exposes a few interesting tidbits on the batteries :
  • SoC %
  • "Remaining Capacity"
  • "Label Capacity"
  • Actual voltage
  • Current going in / out during charging / discharging
  • Temperature (only when charging / discharging so 0 right now)
  • ID of the battery
It also matches exactly what I remembered - Fully charged, older batteries, are showing less than 100% SoC and Remaining Capacity.

I'm not exactly sure how the Nexus does this but I can guess. I sniffed the packets between the tool and the Battery and nothing new showed up.. No new messages. Also remember that the battery's BMS controls the charging. It tells the charger when to stop so the BMS is telling the nexus "I'm full, please stop charging".

Also, look at the Battery voltage. The 7.5Ah which is, in my experience, my most abused one, is sitting at 55.12V

Plugging this into my Nexus 400 and turning it on to sniff I get this :

1778771481902.png

As you can see the cells are all around 3.95V... and the packed stopped charging.

At first I thought "degradation".. but it's an odd way to deal with it. My experience with degraded Lithium cells is that their internal resistance goes up so you get less Ah out of them but you can still fully charge them up to 4.2V.

Confirmed with a multimeter - battery is sitting at around 55.1-55.2 V so it's not the BMS fudging the numbers to appear degraded.

2 theories right now :
  1. Pack needs to "trickle charge" past the "charging complete" like my other pack on a regular charger did. Need to drain it a bit and put it on a charger instead of the Nexus station. Will do that right now
  2. The IR is so high that the cells see 4.2V for a while during charging
edit : oh, and before anyone asks, I'm NOT going to publish that code on GitHub. While there's an argument that can be made that I bought the tool and the functionality was lost when EGO abandoned the App... and lost functionality meaning even the local BLE... decompiling code isn't as clear cut, legally, vs sniffing traffic on a digital line...
 
Last edited:
2 theories right now :
  1. Pack needs to "trickle charge" past the "charging complete" like my other pack on a regular charger did. Need to drain it a bit and put it on a charger instead of the Nexus station. Will do that right now
  2. The IR is so high that the cells see 4.2V for a while during charging
So it was #1.

Charging on both the Nexus 400 and Nexus 3000 isn't like on a an actual charger.

I ran the battery around 1 min at full blast on my leaf blower then put it on the normal slow charger. Did a full capture of packets.
  • Within 10sec, the charging handshake was done
    • START_
    • EVACHG
    • GETCG1 / OUTCG1
    • GET_VM / OUT_VM (60V)
    • GET_CM / OUT_CM (3A)
    • EN_OUT
    • CHGING <-- Charging then starts
  • Current gradually goes up to around 3A and fan goes up / down
  • 15min in, the battery sends a "CHGFUL"
    • Battery light is now off
    • Charger light shows fully charged
    • Battery was still pulling 2.67A at this point
  • 67min in (42min after the charger reports full battery)it actually shuts off
    • Current slowly went down from 2.67 to 0.28A
    • Then it abruptly went to 0A followed in quick succession of :
    • FANOFF
    • DISOUT
    • CHGFUL
    • SLEEP_
I now get this from the Nexus 3000 :

Code:
  Batteries:
    B1: loaded       87%   4.3Ah/5.0Ah   56.72V   0.00A  T=  0
        ID: 00000000000023D02A06
    B2: loaded      100%  10.0Ah/10.0Ah   58.16V   0.00A  T=  0
        ID: 000000000000AB592369
    B3: loaded       98%   7.3Ah/7.5Ah   58.28V   0.00A  T=  0
        ID: 00000000000023F82A79
    B4: (not connected)

Much better... but the SoC % and the Capacity remaining are still a mystery. Pack voltage is higher than the 10Ah pack but SoC is listed at 98% and remaining cap at 7.3Ah
 
I sniffed the packets between the tool and the Battery and nothing new showed up.. No new messages.
So I said this but I realized mid sleep yesterday (having weird dreams about batteries and 1wire stuff lol) that I actually didn't confirm that this was the case.

So I did 2 captures (5 and 7.5Ah) on the Nexus 3000. Turns out the Nexus 3000 actually uses RD_FSH more than the Nexus 400.

I now believe this is probably "Read Flash" or something like that because like the Nexus 400 it sends a RD_FSH for 0x38 but then it sends 4 RF_FSH with incrementing the parameter (0x78,0x79,0x7a then 0x7b). Feels lot like "read flash register 0x78 then 0x79, etc etc".... i.e. scanning for values.

Couldn't make sense of the values so far.

Byte7.5 Ah @ 96 %7.5 Ah @ 37 %5.0 Ah @ 86 %5.0 Ah @ 32 %Notes
0x380xFF (stub)0xFF (stub)0xFF (stub)0xFF (stub)Gen 1 marker
0x780x23 (35)0x23 (35)0x23 (35)0x23 (35)constant — likely fixed identifier
0x790xF8 (248)0xF8 (248)0xD0 (208)0xD0 (208)per-pack, stable across SoC
0x7A0x2A (42)0x2A (42)0x2A (42)0x2A (42)constant — likely fixed identifier
0x7B0x79 (121)0x79 (121)0x06 (6)0x06 (6)per-pack, stable across SoC

the SoC% is the nexus reported SoC %.

In all 4 cases, 0x78 was returned as 0x23 and 0x7A was returned as 0x2A.
However, 0x79 and 0x7B seems to be pack specific.

0x79 : My 5Ah pack returns 0xD0 or 208 (maybe 2.08Ah left per cell? that's a lot of degradation) and the 7.5Ah one returns 0xF8 (248)
0x7B : 0x06 and 0x79 (6 and 121 respectively). Maybe a charge counter? or Cycle counter? I clearly charged my 7.5Ah more often, it's been my main mower battery for like 7-8 years.

Couple of todos :
  1. do the same with some of my Gen 2 packs and compare. I know my 12Ah packs have less than 5 cycles on them. The 12Ah were used for my snowblower do they have more cycles and probably more degraded
  2. do the same with my 2.5Ah gen 1 or 2 pack (can't recall which one I have but it's at my cottage with the chainsaw so hard to get to). Cell capacity could be lower on these I don't know but it wasn't used much, Like <10 cycles
  3. use the batteries a bit and track full drain + charge cycles and compare the values to see if they change
  4. poke around by sending other bytes to see if we can get something out of these that the Nexus 3000 isn't fetching.
For now, my leading theory is that the Nexus 3000 infers the SoC % with the voltage and uses that to math the remaining Ah capacity but I'm not 100% on this.

Updated both the firmware and the gui repos with this info and some captures
 
I have been periodically looking up to see if anyone had pulled of communication with ego packs. Super excited to see this I will add I have the commercial backpack battery and plan on using it to run a compressor based thermal control system.

I have found SBECs that can handle 14s input and output at 24v max from a brand I trust. They claim 32amp max Link below.

 
Back
Top