Sofar Solar ME3000SP Firmware Upgrade Procedure

Here are my notes on upgrading the firmware. Thanks to Tim who posted in the comments on this page: https://www.setfirelabs.com/building-automation/sofar-solar-me3000sp-battery-inverters and confirmed that with the new formware, the Depth of Discharge can now go to 90% with the Pylontech batteries.

Sofar ME3000SP Inverter

I had never previously changed the firmware which was 2.40 but have now been through the process on my inverters and documented it here to I can refer to it next time.

Step 0 – Did you freeze your CTs?

The firmware upgrade requires that you power down your inverter. You should ensure you have your CT settings frozen first – see this page for instructions (setting up CTs section).

Step 1 – Power down the inverter

Turn off the AC power to the inverter, pull the fused DC isolator if you have one or turn off your complete battery pile.

AC Isolator
DC Fused Isolator

Step 2 – Remove communications cover

Remove the waterproof cover to the communications cables (loosen the glands first).

Remove the communications cover

Step 3 – Remove the microSD card

Inside the cover is the microSD card slot, which should have a card already in it. Press the card edge until it clicks. Release and it will then drop out of its slot enough so you can grab hold of it and withdraw it. The card is formatted MSDOS FAT32 and my card has CSV files on it with daily/monthly logging.

SD card location

Step 4 – Set up a new SD Card with the new firmware

Tim suggested in the comments that upgrading to 3.00+ firmware should be performed in 2 steps – first upgrade to 3.00 and then beyond. I thought I would give it a go straight to 3.06.. which worked fine.

Firmware file structure

To do this, you need to download the firmware from here and prepare the micro SD card. Unzip the appropriate firmware so you have an ES3000firmware folder with .bin files in it.

microSD card plus adapter

The microSD card needs to be formatted as MSDOS FAT32. Sofar instructions tell you to format the existing card, but I used another card which already came formatted that way. I used an adapter to plug it into my Macbook.

I copied the ES3000firmware folder to it and then inserted it into the card slot on the inverter (still powered down).

Next power the inverter back up. DC first, then wait 5 seconds then AC. It should boot as normal and then begin operation as if nothing had happened.

Step 5 – The actual upgrade

Use the back button to get the menu up, then select 4. Software Update.

Software Update

Press enter, then put in the password 0715.

Here is the successful update sequence. Once the password has been correctly entered, you don’t need to do anything else.

Should really read ‘Update has started’
The update has finished.

You can confirm the update worked:

System Information
Software version
Software version is now 3.06

Step 6 – Did it fail?

My success rate first time round was 1 out of 3. All my failures were on DPS2:

The update continues after DSP2 fails and the inverter reboots. However, the battery communications don’t work so this is not a satisfactory state to leave the inverter in.

If this happens, return to the start of Step 5 and try again.

Step 7 – Power down / replace card / restart

At this point, the inverter had started up and was operating normally. I then powered it down again, then swapped the microSD card for the original one. If you put the firmware on the original card, you wouldn’t need to do this. The Sofar instructions do say to power it down and restart though.

Successful update – new features

Moving from 2.40 to 3.06 gave me two new features – a new battery parameter with the ability to select US3000 batteries:

Battery parameter
US3000

Plus depth of discharge now up to 90%:

Depth of Discharge
DOD 90%

Pylontech US2000 / US3000 CAN reading and CAN replication

Following on from the previous post where I was able to read the CAN data, here is a sketch for the NodeMCU and MCP2515_CAN modules to collect the CAN data, interpret it, post it to emoncms, and also replicate it onto additional CAN busses for systems with multiple Sofar Solar ME3000SP inverter/chargers.

Pylontech CAN data reporting and replication

I had planned a simple sketch first, but reliably reading a complete set of CAN data proved less simple than I had imagined.

Catching all the CAN data

The Pylontech battery stack outputs its CAN data once per second. This consists of six CAN packets like this:

CAN ID – followed by 2 to 8 bytes of data:
0x351 – 14 02 74 0E 74 0E CC 01 – Battery voltage + current limits
0x355 – 1A 00 64 00 – State of Health (SOH) / State of Charge (SOC)
0x356 – 4e 13 02 03 04 05 – Voltage / Current / Temp
0x359 – 00 00 00 00 0A 50 4E – Protection & Alarm flags
0x35C – C0 00 – Battery charge request flags
0x35E – 50 59 4C 4F 4E 20 20 20 – Manufacturer name (“PYLON “)

If you are watching the bus, you will also see a 0x305 ID message which is output by the ME3000 once per second. The Pylontech data is sent fairly rapidly, and as was noted in the previous post, and is easy to loose. The MCP2515 has only two receive buffers and so can only hold two messages. If both buffers are full, any additional messages are ignored so the code must clear the buffers quickly during the stream of six messages from the batteries. The messages are always sent in the same order, and it is always the same messages which are lost if the code is too slow in checking. I suspect there are a couple which arrive very soon after the previous message is sent. The priority for the sketch is to collect and buffer the messages, and then process them less frequently.

Here is the first part of loop():

  // If CAN0_INT pin is low, read receive buffer
  if (!digitalRead(CAN0_INT)) {
    while ( CAN0.readMsgBuf(&rxId, &len, rxBuf) != CAN_NOMSG ) {
      store_can();
    }
  }

This is slightly different to the example code for the library (https://github.com/coryjfowler/MCP_CAN_lib/) which does only one check/read per loop() and was found to drop messages – and always the same IDs which the Pylontechs seem to deliver very soon after the previous message. The method above has been very reliable. The routine store_can() puts the CAN message in our own (larger) circular message buffer and does no post-processing – which would make it too slow. Prior to using this polling method in the main loop, I tried an interrupt routine to store to the buffer but this required a different library (which itself needed fixing) and caused new problems with clearing interrupt flags on the MCP2515 – resulting in more lost messages! Polling the interrupt pin on the MCP2515 has been the most reliable method I have found.

Buffering CAN Messages

It is important to buffer the CAN messages so we hold at least one full set of six representing the complete picture of the batteries via CAN. If we have the compete picture, we can pass it to emoncms for reporting. We can also relay it onto additional CAN buses for additional inverter/chargers. The sketch can also print the buffer to the serial port periodically so you can see what is happening (look for the setting INTERVAL_DISPLAY). Each line shows the buffer position, ID, 8 bytes of data, and 2x data length (first for emon post, second for bus replication):

7 35E 00 00 00 00 00 00 00 00 08 08
6 35C 00 00 00 00 00 00 00 00 08 08
5 356 00 00 00 00 0A 50 4E 00 07 07
4 355 14 02 74 0E 74 0E CC 01 08 08
3 351 0E 00 64 00 00 00 00 00 04 04
2 359 02 13 00 00 4A 01 00 00 06 06
1 305 C0 00 00 00 00 00 00 00 02 02
0 305 50 59 4C 4F 4E 20 20 20 08 08
31 35E 00 00 00 00 00 00 00 00 08 08
30 35C 00 00 00 00 00 00 00 00 08 08
29 356 00 00 00 00 0A 50 4E 00 07 00
28 355 14 02 74 0E 74 0E CC 01 08 00
27 351 0E 00 64 00 00 00 00 00 04 00
26 359 02 13 00 00 4A 01 00 00 06 00
25 305 C0 00 00 00 00 00 00 00 02 00
24 305 50 59 4C 4F 4E 20 20 20 08 00
23 35E 00 00 00 00 00 00 00 00 08 08
22 35C 00 00 00 00 00 00 00 00 08 08
21 356 00 00 00 00 0A 50 4E 00 07 07
20 355 14 02 74 0E 74 0E CC 01 08 08
19 351 0E 00 64 00 00 00 00 00 04 04
18 359 02 13 00 00 4A 01 00 00 06 06
17 305 C0 00 00 00 00 00 00 00 02 02
16 305 50 59 4C 4F 4E 20 20 20 08 08
15 35E 00 00 00 00 00 00 00 00 08 08
14 35C 00 00 00 00 00 00 00 00 08 08
13 356 00 00 00 00 0A 50 4E 00 07 07
12 355 14 02 74 0E 74 0E CC 01 08 08
11 351 0E 00 64 00 00 00 00 00 04 04
10 359 02 13 00 00 4A 01 00 00 06 06
9 305 C0 00 00 00 00 00 00 00 02 02
8 305 50 59 4C 4F 4E 20 20 20 08 08

Hardware

NodeMCU and MCP2515 CAN modules

The NodeMCU is connected to the 3x MCP2515_CAN modules in the same way as in the previous post. The only differences here are that the 3.3V supply to them is via a AMS1117-3.3 LDO 5v to 3.3V converter module which is fed from the 5V USB supply. I added 10uF 25V capacitors to the 5V and 3.3V output rails to ensure a stable supply (probably didn’t need these, but I considered it good practice). I am using I/O pins D1, D2, D3, D4 for CAN0 INT, CAN0 CS, CAN1 CS, CAN2 CS. D4 is best as a CS function rather than the INT as it is also used for UART communication during programming. The INT output on the MCP2515_CAN module would be pulled low if there was data on the CAN bus and this would interfere with programming if on an active CAN bus.

Some notes about the system and sketch

Firstly, messing around with your battery system is not a good idea unless you really, really, know what you are doing. You will most likely invalidate any warranties. You also risk:

  • destroying your battery system.
  • destroying your inverter/chargers.
  • burning your house down.
  • something worse.

YOU HAVE BEEN WARNED!

The connection to the first CAN bus for reading the data is done using the first CAN interface in listen-only mode and the CAN interface on the Pylontech batteries still connects to the first inverter. The interface also has no bus termination as this is provided at the two ends as usual – the battery, and the inverter. By connecting to the CAN bus this way, we ensure the first inverter is always connected to the batteries. This is important as it ensures the batteries always have a system looking after their interest – like making sure the charge never gets too low (the batteries can force the inverter to charge them from mains if they get too low).

There is one problem inherent in this connection method – the maximum charge and discharge currents are reported to the inverters as-is. This means there is the potential to over charge or over-discharge the batteries. For a small system, this could be a problem and should be considered. For example:

  • 2x US2000 batteries = 2x 20A = 40A maximum charge and discharge as reported by the batteries to the inverter.
  • if you have 3x inverters, each is sent 40A maximum charge/discharge setting.
  • Therefore theoretical maximum charge/discharge current at the batteries is 3x40A = 120A. BAD. You will need to manually reduce your maximum charge and discharge settings on each inverter.

However, if you have a stack of 10x US3000 batteries:

  • 10x US3000 batteries = 10x 37A = 370A charge and discharge as reported by the batteries to the inverter.
  • if you have 3x ME3000SP inverters, each is capable of 65A max. charge/discharge.
  • Total theoretical charge/discharge for the batteries is 3x 65A = 195A which is well within the 370A maximum.
  • Of course, your battery cabling needs to be up to the job:
Battery protection
How to connect 10x Pylontech battery leads to 3x inverters. Terminal covers removed for photo!

The sketch

The sketch can be found here: https://github.com/setfirelabs/Pylontech_emoncms REMEMBER THE WARNINGS ABOVE BEFORE MESSING WITH YOUR SYSTEM!

The serial output should look something like this:

Ready
IP address: 192.168.1.115
Entering Configuration Mode Successful!
Setting Baudrate Successful!
Entering Configuration Mode Successful!
Setting Baudrate Successful!
Entering Configuration Mode Successful!
Setting Baudrate Successful!
Rx: 0x359
Rx: 0x351
Rx: 0x355
Rx: 0x356
Rx: 0x35C
Rx: 0x35E
Rx: 0x305
Rx: 0x305
Rx: 0x359
Rx: 0x351
Rx: 0x355
Rx: 0x356
Rx: 0x35C
Rx: 0x35E
Rx: 0x305
Rx: 0x305
Rx: 0x359
Rx: 0x351
Rx: 0x355
Rx: 0x356
Rx: 0x35C
Rx: 0x35E
Rx: 0x305

Tx: 0x35E len: 8 50 59 4C 4F 4E 20 20 20 (ok) (ok)
Tx: 0x35C len: 2 C0 0 (ok) (ok)
Tx: 0x356 len: 6 2 13 0 0 4A 1 (ok) (ok)
Tx: 0x355 len: 4 E 0 64 0 (ok) (ok)
Tx: 0x351 len: 8 14 2 74 E 74 E CC 1 (ok) (ok)
Tx: 0x359 len: 7 0 0 0 0 A 50 4E (ok) (ok)

..and have fun graphing!

emoncms pylontech graph
emoncms pylontech graph

Multiple Sofar ME3000SP inverters for 3-phase setup?

(or multiple inverter/chargers connected to one battery pile)

Is it possible to connect multiple ME3000s to one (Pylontech) battery pile? The short answer is: yes, it is possible, but it doesn’t work all that well.

And don’t blame us if you damage your batteries / set fire to your house / etc. The use of this information is entirely at your own risk!

The US2000 / US3000 batteries talk to the Sofar Solar ME3000 inverter/charger using the CAN interface. Theoretically, once the first one inverter/charger is connected to the batteries using CAN and a battery setting of PYLON, any subsequent inverter/chargers can use a battery type of DEFAULT – which is generally used for lead-acid batteries.

Here are some rough battery parameters for the default battery type in this configuration:

  1. Battery type: DEFAULT
  2. Battery Capacity: (whatever your battery pile is)
  3. Discharge Depth: 80%
  4. Max Charge Current: (calculated as your battery capacity divided by number of ME3000s)
  5. Over Voltage Protection: 54.0V
  6. Max Charge Voltage: 51.5V (normally 52.8V but limited to stop over-voltage – see below)
  7. Max Discharge Current: (same as Max Charge Current)
  8. Low Voltage Protection: 45.9V
  9. Min Discharge Voltage: 49.0V (normally 47.2V but higher here to limit over-discharge)
  10. Empty Discharge Voltage: 46.7V
  11. Full Charge Voltage: 52.08V
  12. You also need to have the temperature probe connected (and ideally attached the to case of the top battery pack).

Limitations

Using these settings with a different battery technology poses a number of problems around the battery-protecting charging and discharging limits and also knowing the state of charge (SOC).

Voltage measurement

Knowing the voltage of the batteries is important since this is used by the inverter/charger to ensure the batteries are kept within their operating limits, which ensures they have a long and useful life.

When using the DEFAULT battery setting, this voltage is measured inside the inverter, not inside the batteries. This causes a problem in that the more current is drawn from the batteries, the more the voltage drops due to cable and internal battery resistance. The boffins at Sofar Solar have thought of this though – and they compensate for it. However, they have no idea what the resistance of the cables is, and they think the battery is lead-acid. Have a look at this graph from an ME3000 in DEFAULT mode produced by Solarman covering a 24 hour period and showing SOC, Voltage and Current. The battery current is shown as red in the graph and starts the day at about zero. This is for phase L1.

Solarman Graph L1
Solarman Graph Using DEFAULT battery type – phase L1

Look at the two negative peaks at about 02:00 and 04:30. When the current is drawn, the voltage goes up! (Specifically, the first 26A draw causes a 1.1V increase, the second 30A draw causes a 1.2V increase.) A battery will not behave like this in real life – clearly the system is over-compensating for the resistance. This has a knock-on effect in our settings – we want to protect the batteries by stopping the voltage dropping too far when the SOC is low, which is done with the Minimum Discharge Voltage. For the PYLON type battery setting, this is minimum is 47.2V and so for DEFAULT we are using 49.0V to compensate in the settings above. This will be system-specific since it depends on battery size, likely current draw and battery cables.

The reverse can also be seen in the charging phase of the day – the voltage drops when charging and so is all over the place. Because during charging the voltage measurement is being artificially reduced, we need to compensate in the Maximum Charge Voltage by reducing it.

Compare the graph above to the inverter on the same battery pile using CAN for its readings over the same period. This is on L3:

Solarman using CAN
Solarman graph from ME3000 using CAN – phase L3

..the early current spike causes no change in the voltage and overall the voltage is far more stable.

State of Charge (SOC) calculations

OK – the inverter can see the battery, its voltage, knows the current being drawn or charged, and knows the capacity of the battery. However, it doesn’t know what any other inverter/chargers are doing and assumes it is the only one. Battery state of charge (SOC) is a calculated figure, whether the batteries are calculating and reporting it, or the inverter/charger is. In our case here, using the DEFAULT battery type, the inverter is attempting to work out the SOC. It only knows about itself, and so its calculations will not be accurate (whereas the battery management in the Pylontech batteries have the full picture).

You can see by comparing the graphs above that the SOC doesn’t look too different. The L3 graph is the accurate one, and the L1 one above shows a similar curve as the battery is charged. However, at the start of the graph (midnight) L1 shows the SOC too low, and after the battery pile is fully charged (15:30) it then remains too high.

Now for three graphs showing L1, L2, L3 on a different day.

The first graph shows L3 – which is connected via CAN to the batteries and shows the true picture:

Solarman graph - L3
Solarman graph – L3 – CAN connected to Pylontech battery pile.

SOC starts at 64% at midnight, dropping to 27% at 13:57 before starting to recharge, where it hits 72% at 18:13 before starting its discharge gain. Throughout the day the inverter is either discharging or charging the battery as the demand or excess on that phase requires. [Note that there is a display problem on the graph – the current scale is incorrect here – 0A is shown as about -15A.]

Here is L1 for the same day:

Solarman graph - L1
Solarman graph – L1 (DEFAULT battery type)

The SOC scale is different, but the day starts at 50%, drops to 36% at 10:04, but generally bears no resemblance to the actual SOC. The load on this phase is very constant. [Here the Solarman system is incorrectly reporting -6A when it should be zero.] Here is L2:

Solarman graph - L2
Solarman graph – L2 – DEFAULT battery type.

The day starts at 53%, drops to 42% at 03:38, recovers a bit and then starts climbing as charging starts at 11:50, reaching 78% at 18:39. The inverters don’t really have a clue what the SOC is, but seem to be periodically recalculating it based on the changing battery voltage.

Charge rates

For good battery management, charge rates vary depending on SOC. When eg. for LiFePO4 batteries, they are charged at 0.5C until they are close to their maximum charge (90%), then the charge rate is reduced to something like 0.2C. In order for the charger to reduce the charge rate at close to full charge, it needs to accurately know the state of charge.. which we have shown above is not the case with the DEFAULT setting.

Continuing to charge at 0.5C past 90% state of charge is likely to shorten the life of your batteries. But – the charge rate could already be limited by your charger. Each US2000 has a maximum charge rate of 20A and each US3000 a maximum charge of 37A; this is the 0.5C. We can therefore calculate the 0.2C which would be 8A for the US2000 and 14.8A for the US3000. Let’s say you had 10x US3000 – 0.2C would be 148A. If you had 3x ME3000s each set at 50A max charge, you would never exceed 0.2C anyway and so would be safe. If you had 4x US3000, 0.2C would be 4 x 14.8 = 59.2A so you should be more careful. As we have seen above, attempting to control the charge using the battery voltage is difficult because of software compensation of the voltage – during charging the voltage is suppressed. Of course, if your batteries tend not to get close to full charge, this is less of an issue. Also bear in mind that if you have 3x inverter/chargers, one will be connected via CAN and so you are only concerned with the other two. Maybe you can arrange them with your generating equipment so this is not a problem (eg. most generation on the one charger connected via CAN).

Pylontech US2000/US3000 NodeMCU CAN reader

The Pylontech batteries use either CAN or RS485 to communicate with the inverter. This post is a look at the CAN interface, and how to read that information to allow output to something like emoncms or MQTT.

Pylontech US2000 Battery
Pylontech US2000

Battery communication via the CAN interface is used by the Sofar Solar ME3000 inverter/charger in the setup we have and so the information here is based on this.

There is a Pylontech document specifying the CAN communications around on the internet although it is not that easy to find, and people connected with or in communication with Pylontech seem reluctant to share is, I think because of a NDA. It is, however, out there and if you know what to search for you can find it (ahem, CAN Bus Protocol Pylon low voltage).

The data is output by the battery (or master of a connected battery stack) every 1 second. The data rate is 500kbps and the output information is:

  • Protection Flags (eg. over/under temp)
  • Alarm Flags (eg. over current / voltage, communication fail)
  • Request Flags (eg. force charge)
  • State of Charge and State of Health
  • Voltage, current, Temperature
  • Maximum Voltage, Maximum charge current, Maximum discharge current

The information is for a complete pile (stack of batteries) basis and so there is no information on individual cells, for example.

Reading CAN

Since the CAN bus is a bus (!), multiple communicators can be connected to it. The Pylontech and the ME3000 inverter are the two end-points as they have the resistor terminators. This means another CAN device on the same must be unterminated. (So you can’t for example connect another ME3000 to it and expect it to work.) You can connect into the bus with an unterminated device and read the data without interfering with it. THIS IS GOOD because there is important battery management stuff going on between the inverter/charger and the batteries which you SHOULD NOT MEDDLE WITH. For example, The battery will force a charge from the inverter if it is at a very low state of charge, or if it thinks the state of charge is inaccurate (due to not having reached full charge for a while). Ultimately, you don’t want dead batteries, or a fire on your hands.

MCP2515_CAN Module
MCP2515_CAN Module

The MCP2515_CAN module is a CAN interface which communicates with the NodeMCU via SPI. It can also be powered from 5V or (in our case) 3.3V. J1 is for enabling the terminator resistor so we leave it unconnected. J2 is for the connection to the CAN bus.

SPI (Serial Peripheral Interface) uses four wires for communication. It is a full-duplex, master-slave interface where only one peripheral can communicate with the master at one time. However, multiple peripherals can be connected – and then a fifth wire is used to select which device to talk to – CS or ‘Chip Select’. 

In addition, an interrupt line can be used for the peripheral to signal that there is something to read from it.

The NodeMCU has two SPI interfaces (SPI and HSPI) built-in to the hardware, but one is used internally (SPI – for flash) and so we use the other – HSPI. This is specified here: https://nodemcu.readthedocs.io/en/release/modules/spi/

On the NodeMCU V0.9 board, the useable SPI interface was labelled:

NodeMCU V0.9
NodeMCU V0.9 Pinout

But on the V1.0, only the D lines are labelled:

NodeMCU V1.0 SPI Interface
NodeMCU V1.0 SPI Interface

They are both in the same place though. Here are the assignments:

SignalIO indexESP8266 pin
HSPI CLKD5GPIO14
HSPI /CSD8GPIO15
HSPI MOSID7GPIO13
HSPI MISOD6GPIO12

We are using the NodeMCU as the SPI Master device, so MOSI becomes Master Out and MISO becomes Master In.

MCP2515_CAN J4:

  • INT – interrupt
  • SCK – Serial Clock
  • SI – Slave In
  • SO – Slave Out
  • CS – Chip Select
  • GND – Ground power connection
  • VCC – + Volts connection

We connect the Master Out on the NodeMCU to Slave In and Master In to Slave out:

NodeMCU to MCP2515_CAN module
NodeMCU to MCP2515_CAN module

Powering the interface module

Now, the MCP2515_CAN requires that its power supply matches the voltage of the communicating device. With NodeMCU we are running at 3.3V and so the MCP2515_CAN can also be powered from 3.3V. 

HOWEVER, with the NodeMCU board  powered via the USB connector it is creating its own 3.3V via an on-board regulator from the 5V USB input. This is not sufficient to power the MCP2515_CAN for transmissions on the CAN bus. It is enough for just reading the bus though and so you can see we have powered the board from a 3.3V pin on the NodeMCU. (If you needed more power then a step-down module such as the AMS1117-3.3 LDO module could be connected to the 5V pin to provide the 3.3V for the MCP2515_CAN.

Connecting to the CAN bus

The CAN bus connector on the Pylontech battery is an RJ45 connector – the same as used for networking. CAN uses two wires and these are the blue pair – the centre two pins of the connector. To connect into the bus, we can use a pair of RJ45 sockets connected together with the pair connected to our CAN input:

CAN RJ45 connections
CAN RJ45 connections

All hardware is now ready! It can be tested using the coryjfowler library https://github.com/coryjfowler/MCP_CAN_lib and the example sketch CAN_receive.ino with the CS and INT data pins set to match ours:

#define CAN0_INT D1
MCP_CAN CAN0(D2);

Now look for the line: if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK) and if you have an 8MHz crystal on your MCP2515_CAN module, change it to:

if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_8HZ) == CAN_OK)

Connecting into to the CAN bus with an RJ45 cable from the top battery CAN port to our RJ45 socket, and then connecting the inverter comms cable to the other RJ45 puts us on the CAN bus, and so the sketch can be uploaded and run. If you have been successful, you should get some CAN data in your serial monitor like:

Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E

A couple of things to point out here: outputting all this junk to the serial port is causing some packets to be missed – the loop is too slow and the CAN buffer is not cleared in time to receive them all (eg. there is no 0x35C ID here) and also the CAN_receive.ino sketch puts our CAN interface into MCP_NORMAL mode – which sends ACKs (we should really be in CAN_LISTEN mode).

Next post – a sketch to decode this data and send it somewhere useful..

Update – post is here: Complete sketch for reading and posting the Pylontech CAN data to emoncms, plus replication to additional CAN busses.

Sofar Solar ME3000SP Battery Inverters – installation tips

Sofar ME3000SP Inverter

Some useful things about this inverter which are not mentioned, or not obvious from the manual. This is based on experience with software version 2.4.

Connecting things up

You will need a replacement 20mm cable gland for the mains connection(s) if you are using a 3-core flex as the bung in each cable gland has 3x small holes and so expects 3x single core cables.

Mains cable gland

The unit comes with 2x M6 cable lugs for 25sqmm DC cables but M8 lugs fit fine (eg. on the end of the Pylontech cables).

Setting up the CTs

The unit comes with 2x CT clamps:

CT clamp

One is for the supply and should be clipped on to your incoming supply live (big fat single-core cable next to your meter). The other is to monitor your generation and so clips to the live wire supply to your PV panels (inside your generation board etc.).

The setup procedure for your inverter includes the steps:

  1. Make sure all PV is off.
  2. Turn on battery supply to inverter.
  3. Turn on mains supply to inverter.
  4. (now set up time/grid profile/battery)
  5. Make sure you are consuming at least 200W.
  6. Turn on the PV and watch it on the display.

What has actually happened here is that the system has worked out which way round the current clamps are (remember you didn’t have to install them a particular way round?)

Now the important part: if you switch off and restart the system, it will go through the same CT configuration again. In fact, there appear to be a number of ‘events’ which cause the CT reconfig:

  • Mains off/on
  • Battery off/on
  • Change work mode

That means that if you cause the CT reconfig when you are exporting power, it will incorrectly read this as an import and then start exporting until it reaches maximum export. Imagine you are currently exporting 2.5kW from your PV – this then ramps up to 2.5kW plus another 3kW from the inverter = 5.5kW export! You do have a G99 relay to stop this don’t you?!?

There is a way round this though: FREEZE CT. This is something you should do when you are happy it is configured correctly. Go to Configuration, then down to option 13:

13 - CT configuration

Enter the password:

Enter password

You will see the CTs, and at the bottom ‘UNFREEZE’ (which counter-intuitively means ‘CTs not frozen’:

CT Unfreeze

Press the down button to display ‘FREEZE’:

CT Freeze

..and then press ‘enter’ to save (then enter again to get off the confirmation screen). You can check the CT direction is frozen from System Information, screen 4:

System Information

..which should now show ‘Frozen’:

System Information 4

CTpv isn’t needed

It is possible to use this inverter without the pv sensor. It appears the CTa clamp is the only one used for import/export. This is good news for people who are not able to monitor all (or any) of their generation due to it being connected into different parts of the mains system without a central point for a CT. Obviously you won’t get logged data for the generation and various totals will not be accurate on the system.

Datalogger

The inverter comes with a datalogger with WiFi. This is something not mentioned in the manual for the inverter. It connects to a DB9 port on the bottom of the unit and once you have it connected to your wifi network, it posts the unit’s data to the Solarman cloud-based system.

Datalogger

Pylontech US3000 and battery capacity

The inverter doesn’t seem to be able to tell the difference between the US2000 and US3000 on the CAN bus at the moment and so incorrectly sets the capacity to 50Ah per battery instead of 75Ah for the larger US3000.

Changing battery settings

Beware: In order to change battery settings, you have to start by selecting the battery type – at this point, the defaults are loaded, overwriting whatever tweaks you previously made. Make sure you go through each parameter again!

Ultrasonic range measurement for tank level – good idea or not?

Not. I have found it impossible to get good, consistent and reliable readings from an ultrasonic based system used to monitor a water tank . Reasons include:

  • spurious results reported by the hardware
  • anything causing reflections above the water level interferes with what should be the stronger reflection from the actual water.
  • Spider webs!
  • Stillage tube with condensation on the inside
  • Stillage tube crud inside at high-water mark

..so to conclude, next step is to come up with a capacitance based measuring device. Watch this space.

Dealing with DYP-ME007Y TX spurious readings

The serial-output of the DYP-ME007Y ultrasonic range detector is pretty good when you are measuring the distance to a static object with little or no other obstacles creating ‘noise’.

However, the odd spurious reading does crop in from time to time. For example:

201cm
201cm
201cm
128cm
201cm
201cm
201cm
201cm
201cm
201cm

This is actual data, and the spurious 128cm makes no sense and needs to be ignored (rather than just taking an average). Fortunately, @JMMS-Karunarathne and @MikeMeinz wrote some code to correct for this here: https://www.codeproject.com/tips/813946/a-method-to-ignore-anomalies-in-an-ultrasonic-dist

The code takes a group of samples and counts how many of each there are. It then picks the measurement with the highest count (the ‘mode’). In the above example, there are 9 counts of 201cm – which would be returned as the measurement.

So far, so good..

However, there is a problem where all the data is rubbish. Look at this actual data:

275cm
273cm
273cm
275cm
0cm
483cm
0cm
0cm
125cm
235cm
274cm
274cm
274cm
690cm
816cm
274cm
274cm
273cm
0cm
483cm
0cm
274cm
274cm
90cm
184cm
292cm
273cm
539cm
648cm
316cm

This was produced by out-of-range measurements – the object was less than 28cm from the sensor. The measurement which occurred the most was 274cm and so the algorithm would return this seemingly meaningful result.

An improvement to the ‘mode’ routine would be to apply a ‘confidence’ to the measurement. In my code, I take 30 readings, and require a minimum of 20 of the same to accept it as a good measurement. I try this five times over, after which the routine gives up and returns a zero indicating out-of-range. This seems to work well.

Except some of the time. This looks like good data:

82cm
82cm
82cm
82cm
83cm
82cm
82cm
83cm
83cm
83cm
83cm
83cm
83cm
83cm
83cm
82cm
83cm
83cm
83cm
82cm
82cm
83cm
82cm
83cm
82cm
80cm
83cm
83cm
82cm
82cm
confidence was 16
Returning 0cm

Here 83cm was only measured 16 times. This was an absolutely static measurement on the bench, however the returned value was either 82 or 83cm (ignoring the 1x 80cm!).

The way to deal with this is to look at the readings +1 or -1 from the mode. If either the count of the mode, added to the count of the mode+1, or mode-1 adds up to at least the threshold, then the result is good.

UPDATE:

Here is my code to do all this and post the result to an emoncms system using an EPS8266. As you will see, I adapted code from https://www.codeproject.com/tips/813946/a-method-to-ignore-anomalies-in-an-ultrasonic-dist

 

/*
This sketch sends data via HTTP GET requests to emoncms.
Reads a DYP-ME007Y TX ultrasonic sensor to output distance
data to emoncms
*/

//uncomment this for dev mode
#define DEVMODE 1

// uncomment so disable sending a result when it is the same as the last one (power save etc.)
// #define NO_SEND_SAME 1

#include <ESP8266WiFi.h>
#include <SoftwareSerial.h>

// pin assignments. TX on the arduino connects to RX on the sensor, RX to TX.
#define TX_PIN D6 // D3 // D5
#define RX_PIN D5 // D4 // D6
#define PWR_PIN D3 // for controlling the power to the Ultrasonic.

SoftwareSerial DYPSensor = SoftwareSerial(RX_PIN, TX_PIN);

#define MAX_TRY_SERIAL 50 // how many cycles of 10ms to wait for before giving up on the sensor (up to 255)
#define MAX_TRY_READING 100 // how many times to try getting a confident reading before giving up and reporting zero (up to 255).

// To collect 30 samples and pick the one with the most votes
#define SAMPLES 30 // Defines how many samples are taken before determining the value that appeared the most and then reporting a reading.
#define MIN_VOTES 20 // How many samples need to be the same to return a good result (suggest 2/3 of SAMPLES)

// define blink error numbers..
const int ERROR_WIFI = 4;
const int ERROR_SENSORS = 5;
const int ERROR_HTTP = 6;

const char* ssid = “YOUR_WIFI_SSID”;
const char* password = “YOUR_WIFI_PASSWORD”;

const char* host = “YOUR_EMONCMS_SERVER”;
const char* apikey = “YOUR_EMONCMS_API_KEY”;
const byte node_id = 35;
const int READING_INTERVAL_SECS = 60; // Default = “60” How long to wait between readings, in seconds.

// Global variables
int SamplesIndex = SAMPLES – 1;
int testValues[SAMPLES];
int testVotes[SAMPLES];
long duration;
int distance = 0;
int last_distance = -1;

void setup() {

#if defined(DEVMODE)
Serial.begin(115200);
#endif

pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output
digitalWrite(PWR_PIN, LOW); // to control power to the sensor

delay(10);

// We start by connecting to a WiFi network
#if defined(DEVMODE)
Serial.println();
Serial.println();
Serial.print(“Connecting to “);
Serial.println(ssid);
#endif

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
#if defined(DEVMODE)
Serial.print(“.”);
#endif
}

// TODO: report error if unable to connect to WiFi network
// using blink code.

DYPSensor.begin (9600);

#if defined(DEVMODE)
Serial.println(“”);
Serial.println(“WiFi connected”);
Serial.println(“IP address: “);
Serial.println(WiFi.localIP());
Serial.println(“Finished Setup.”);
#endif

digitalWrite(LED_BUILTIN, HIGH);

} // end of setup

//
// Initialize arrays used to store values and “votes”
//
void InitializeTestValues()
{
// Initialize test values arrays
for (int idx = 0; idx <= SamplesIndex; idx++)
{
testValues[idx] = -1;
testVotes[idx] = 0;
}
}

//
// Reads bytes from RX port and returns a reading in cm
// Returns -1 until a correct value is available
//
int GetDistance() {
byte msb, lsb, checksum, checkcalc, tries = 0;
int distance;

#if defined(DEVMODE)
Serial.print(” Getting reading..”);
#endif

// we want a 255 which is the start of the reading (not msb but saving a byte of storage)..
while (msb != 255) {
// wait for serial..
while ( not DYPSensor.available() && tries < MAX_TRY_SERIAL ) {
delay(10);
tries++;
}
if (tries == MAX_TRY_SERIAL) {
#if defined(DEVMODE)
Serial.println(” TIMED OUT WAITING FOR SERIAL.”);
#endif
return -1;
}
msb = DYPSensor.read();
}

// now we collect MSB, LSB, Checksum..
while ( not DYPSensor.available() ) {
delay(10);
}
msb = DYPSensor.read();

while ( not DYPSensor.available() ) {
delay(10);
}
lsb = DYPSensor.read();

while ( not DYPSensor.available() ) {
delay(10);
}
checksum = DYPSensor.read();

// is the checksum ok?
checkcalc = 255 + msb + lsb;

if (checksum == checkcalc) {
distance = msb * 256 + lsb;
// Round from mm to cm
distance += 5;
distance = distance / 10;

#if defined(DEVMODE)
Serial.print(distance);
Serial.println(“cm “);
#endif

return distance;

} else {

#if defined(DEVMODE)
Serial.println(“bad checksum – ignoring reading.”);
#endif

return -1;
}

} // end of GetDistance()

//
// Adds a sensor value to the testValues array.
// Adds a vote for that reading to the testVotes array.
//
void AddReading(int x)
{
// Either put the value in the first empty cell or add a vote to an existing value.
for (int idx = 0; idx <= SamplesIndex; idx++)
{
// If an empty cell is found, then it is the first time for this value.
// Therefore, put it into this cell and set the vote to 1
if (testValues[idx] == -1)
{
testValues[idx] = x;
testVotes[idx] = 1;
// Exit the For loop
break;
}
// If the cell was not empty, then check to see if the testValue is equal to the new reading
if (testValues[idx] == x)
{
// Add a vote because we got the same reading
testVotes[idx] = testVotes[idx] + 1;
break;
}
};
}

//
// Finds the highest vote and returns the corresponding sensor value
//
int ReturnHighestVote()
{
float valueMax = 0;
int votesMax = 0;
for (int idx = 0; idx <= SamplesIndex; idx++) {
if (testValues [idx] == -1) {
break; // ignore
}
if (testVotes[idx] > votesMax) {
votesMax = testVotes[idx];
valueMax = testValues[idx];
}
}

#if defined(DEVMODE)
Serial.print(“confidence was “);
Serial.println(votesMax);
#endif

if (votesMax >= MIN_VOTES) {
return valueMax;
} else {
// let’s see of the second highest reading is one away from this one..
// first look at valueMax-1..
for (int idx = 0; idx <= SamplesIndex; idx++) {
if (testValues[idx] == valueMax – 1) {
if (testVotes[idx] + votesMax >= MIN_VOTES) {
// we are ok with valueMax..
Serial.print(“One lower voted “);
Serial.println(testVotes[idx]);
return valueMax;
}

}
if (testValues[idx] == valueMax + 1) {
if (testVotes[idx] + votesMax >= MIN_VOTES) {
// we are ok with valueMax..
Serial.print(“One higher voted “);
Serial.println(testVotes[idx]);
return valueMax;
}
}
}
return 0;
}
}

//
// Calls GetDistance repeatedly, adds it to the “test” arrays.
// After SamplesIndex+1 have been read and inserted into the “test” arrays,
// returns the highest voted sensor value.
// Adapted from original voting routines by @JMMS-Karunarathne and @MikeMeinz
// from https://www.codeproject.com/tips/813946/a-method-to-ignore-anomalies-in-an-ultrasonic-dist
//
int GetSensorValue()
{
int current_reading;
InitializeTestValues();
int tries = 0;
// Get the next Maxsample values from the sensor
for (byte idx = 0; idx <= SamplesIndex; idx++)
{
do
{
current_reading = GetDistance();
if (current_reading == -1) tries++;
// or time out after 20 fails…
} while (current_reading == -1.0 && tries < 20);
AddReading(current_reading);
delay(50);
}
return ReturnHighestVote();
}

void loop() {

byte readings;

// power up the ultrasonic sensor..
digitalWrite(PWR_PIN, HIGH);

// get distance from ultrasonic module..
for (readings = 0; readings < MAX_TRY_READING; readings++) {
distance = GetSensorValue(); // Reads MaxSamples values and returns the one that occurred most frequently
if ( distance > 0) break;

#if defined(DEVMODE)
Serial.println(“TRYING AGAIN..”);
#endif

}
// 0 indicates sensor “Out of Range” condition or low confidence

// power down the ultrasonic sensor..
digitalWrite(PWR_PIN, LOW);

#if defined(NO_SEND_SAME)
// now see if the reading was the same as the last one..
if (distance == last_distance) {
// no need to send the result as it is the same as the last one sent..
return;
}
#endif

#if defined(DEVMODE)
Serial.print(“connecting to “);
Serial.println(host);
#endif

// Use WiFiClient class to create TCP connections
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort)) {
#if defined(DEVMODE)
Serial.println(“connection failed”);
blink_error(ERROR_HTTP);
#endif
// give up and repeat the whole loop..
return;
}

String url = “/emoncms/input/post.json?apikey=”;
url += apikey;
url += “&node=”;
url += node_id;
url += “&json={“;

if (distance == 0) {
#if defined(DEVMODE)
Serial.println(“BAD READING”);
#endif
url += “read_fail:1}”;
} else {
url += “D:”;
url += distance;
url += “,R:”;
url += readings;
url += “}”;
}

 

#if defined(DEVMODE)
Serial.println(“Sending: “);
Serial.print(distance);
Serial.println(“cm”);
Serial.print(“Requesting URL: “);
Serial.println(url);
#endif

// This will send the request to the server
client.print(String(“GET “) + url + ” HTTP/1.1\r\n” +
“Host: ” + host + “\r\n” +
“Connection: close\r\n\r\n”);
delay(2000);

#if defined(DEVMODE)
while (client.available()) {
String line = client.readStringUntil(‘\r’);
Serial.print(line);
}
Serial.println();
Serial.println(“closing connection”);
#endif

// now we have sent it, update last_distance..
last_distance = distance;

 

// quick blink to signify end of cycle..
digitalWrite(LED_BUILTIN, LOW);
delay(100);
digitalWrite(LED_BUILTIN, HIGH);

delay(READING_INTERVAL_SECS * 1000);
}

// report an error by blinking appropriate number (1-255)
void blink_error(uint8_t error_code) {

#if defined(DEVMODE)
Serial.print(“Reporting error: “);
Serial.println(error_code);
#endif

delay(2000);
for (uint8_t i = 0; i < error_code; i++)
{
digitalWrite(LED_BUILTIN, LOW);
delay(400);
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
}

}

DYP-ME007Y TX (Serial output) Ultrasonic Sensor interfacing with Arduino / NodeMCU

There are two main versions of the DYP-ME007Y ultrasonic module (see here to tell the difference) – this article is about interfacing and taking readings from the serial output version.

At the labs, we have been working on interfacing one of these with the NodeMCU – an Arduino-type device with built-in Wifi. This is in order to create a water tank level sensor. The automotive ultrasonic sensor lends itself to this application in that there is no need to add waterproofing – it is already there.

To interface with it we need to convert the 3.3V digital in/outs from the NodeMCU to the 5V required by the DYP-ME007Y. If you were using a 5V Arduino, you wouln’d have to do this. For us, we use a bi-directional logic level converter which is available from eBay. We bought from Electro TV Parts because they stock them in the UK and so shipping is fast.

We are only using two of the four channels on the converter. These are for the two sides of the serial connection between the NodeMCU and the DYP-ME007Y – RX and TX. The converter also needs 5V, 3.3V and GND, which are all available on a pins of the NodeMCU board.

On the NodeMCU, we then need to choose two digital pins for the serial connection, and connect these to the LV (low voltage) side of the converter. The HV side connects to the DYP-ME007Y RX and TX pins. It also needs 5V and GND.

For the serial connection, the pin assigned as TX on the NodeMCU connects (via the converter) to the RX pin on the DYP-ME007Y, and RX on the NodeMCU to TX on the DYP-ME007Y, also via the converter. That’s just how serial works!

Now on to the coding.

We use SoftwareSerial to assign the two digital pins as the TX and RX on the NodeMCU, then we are ready to read the results from the DYP-ME007Y.

#include <SoftwareSerial.h>

// pin assignments. TX on the arduino connects to RX on the sensor, RX to TX.
#define TX_PIN D3
#define RX_PIN D4

SoftwareSerial DYPSensor = SoftwareSerial(RX_PIN, TX_PIN);
const int MAX_TRY_SERIAL = 50; // how many cycles of 10ms to wait for before giving up on the sensor (up to 255)

Here you can see I am using pre-defined constants (D3/D4) for the NodeMCU in the Arduino dev environment. We also define how many times we will try to read the serial before giving up. This allows trapping of the situation were there is nothing coming from the DYP-ME007Y – like if it was not connected.

Now in setup, we start up the serial:

void setup() {

 Serial.begin(19200);
 delay(10);

 DYPSensor.begin(9600);

} // end of setup

We are also starting up the serial connection to the computer it is connected to so we can see debug information.

Our loop looks like this:

void loop() {

 int current_reading;
 current_reading = GetDistance();

 Serial.print(current_reading);
 Serial.println("cm");

 delay(50);
}

..which just calls a subroutine to read the sensor and then prints the result to serial.

GetDistance looks like this:

int GetDistance() {
 byte msb, lsb, checksum, checkcalc, tries = 0;
 int distance;

// we want a 255 which is the start of the reading (not msb but saving a byte of variable storage)..
 while (msb != 255) {
 // wait for serial..
 while ( not DYPSensor.available() && tries < MAX_TRY_SERIAL ) {
 delay(10);
 tries++;
 }
 if (tries == MAX_TRY_SERIAL) {
 Serial.println(" TIMED OUT WAITING FOR SERIAL.");
 return -1;
 }
 msb = DYPSensor.read();
 }

// now we collect MSB, LSB, Checksum..
 while ( not DYPSensor.available() ) {
 delay(10);
 }
 msb = DYPSensor.read();

while ( not DYPSensor.available() ) {
 delay(10);
 }
 lsb = DYPSensor.read();

while ( not DYPSensor.available() ) {
 delay(10);
 }
 checksum = DYPSensor.read();

// is the checksum ok?
 checkcalc = 255 + msb + lsb;

if (checksum == checkcalc) {
 distance = msb * 256 + lsb;
 // Round from mm to cm
 distance += 5;
 distance = distance / 10;

return distance;
 } else {
 Serial.println("bad checksum - ignoring reading.");
 return -1;
 }

} // end of GetDistance()

GetDistance() returns a signed integer of the distance in cm, or -1 if it returned bad data, or the serial read timed out.

It works like this:

  1. Keep reading until we receive a 0xFF which is the start byte.
  2. Now read the most significant byte (msb).
  3. Read the least significant byte (lsb).
  4. Read the checksum.
  5. Calculate the checksum and compare to the one sent from the device.
  6. If checksum matches, it’s a good reading and so return it. If not, return -1.

It makes no attempt to decide whether the sensor has actually sent a meaningful result, just that what we received matched what it sent. More of validating the results in a later article.

Here is the code in full:

 

/*
 Reads a DYP-ME007Y TX ultrasonic sensor and writes the distance to serial.
 This is the SERIAL VERSION of the sensor
*/

#include <SoftwareSerial.h>

// pin assignments. TX on the Arduino connects to RX on the sensor, RX to TX.
#define TX_PIN D3
#define RX_PIN D4

SoftwareSerial DYPSensor = SoftwareSerial(RX_PIN, TX_PIN);

const int MAX_TRY_SERIAL = 50; // how many cycles of 10ms to wait for before giving up on the sensor (up to 255)

void setup() {

Serial.begin(19200);
 delay(10);

 DYPSensor.begin (9600);

} // end of setup


//
// Reads bytes from RX port and returns a reading in cm

int GetDistance() {
 byte msb, lsb, checksum, checkcalc, tries = 0;
 int distance;

// we want a 255 which is the start of the reading (not msb but saving a byte of variable storage)..
 while (msb != 255) {
 // wait for serial..
 while ( not DYPSensor.available() && tries < MAX_TRY_SERIAL ) {
 delay(10);
 tries++;
 }
 if (tries == MAX_TRY_SERIAL) {
 Serial.println(" TIMED OUT WAITING FOR SERIAL.");
 return -1;
 }
 msb = DYPSensor.read();
 }

// now we collect MSB, LSB, Checksum..
 while ( not DYPSensor.available() ) {
 delay(10);
 }
 msb = DYPSensor.read();

while ( not DYPSensor.available() ) {
 delay(10);
 }
 lsb = DYPSensor.read();

while ( not DYPSensor.available() ) {
 delay(10);
 }
 checksum = DYPSensor.read();

// is the checksum ok?
 checkcalc = 255 + msb + lsb;

if (checksum == checkcalc) {
 distance = msb * 256 + lsb;
 // Round from mm to cm
 distance += 5;
 distance = distance / 10;

return distance;
 } else {
 Serial.println("bad checksum - ignoring reading.");
 return -1;
 }

} // end of GetDistance()



void loop() {

int current_reading;
 current_reading = GetDistance();

Serial.print(current_reading);
 Serial.println("cm");

delay(50);
}

 

DYP-ME007Y Ultrasonic distance sensor – PWM or Serial?

In order to build a tank level monitoring device, we recently bought a pile of DYP-ME007Y ultrasonic devices. We chose the single sensor version with the automotive sensor used for car parking distance when mounted on the bumper.

Having tried to use both the manual ‘pulse-trigger then measure the response’ and NewPing code, we gave up.

It turns out that there are two (well, actually three) different versions of this module, and it is not that obvious which one is which.

The two versions are Pulse Width Modulation (PWM) and Serial.

The PWM one requires a trigger input to fire it and this flashes the LED once. You then read the pulse from the Echo output.

The Serial version constantly takes readings, and outputs a distance value in mm in serial. The constant readings result in a flashing LED as soon as you apply power.

So – simple test when you apply power only:

No flashes – PWM version

Flashes – Serial version

The PWM version is well-documented, and easy to interface with on Arduino using the NewPing library but the Serial version is not. There is no way of telling from the markings on the board as these show the pin assignments for both versions.

For specification, look for the DYP-ME007TX which shows:

  • Supply voltage 5 v
  • Global Current Consumption 15 mA
  • Ultrasonic Frequency 40k Hz
  • Maximal Range 400 cm
  • Minimal Range 3 cm
  • Resolution 1 cm
  • Baud rate 9600 n,8,1
  • Outline Dimension 43x20x15 mm

However, our testing shows with the automotive sensor the minimum distance is 30cm.

Also useful is the spec of the output which is four 8-bit bytes:

  1. 0xFF: frame start marker byte.
  2. H_DATA: distance data of high eight.
  3. L_DATA: distance data of low 8 bits.
  4. Checksum byte: Value should equal 0xFF + H_DATA + L_DATA  (only lowest 8 bits)

Sonos CR200 dead spot fault – is it home-repairable?

Sonos CR200 dead pileIf you don’t want to read on, the short answer is ‘no’.

I have had a number of CR200 Sonos controllers fail with the ‘dead spot’ fault. This is a common fault on these controllers, the other being the ‘ghost touch’ fault where the controller appears to be operated by a ghost when noone is near it. It is not fun to be woken up in the middle of the night by loud music in your bedroom when this happens! It seems like these two problems have caused Sonos to abandon manufacturing their own controllers; you can only control their system with either their desktop app on your computer, or an iOS or Android app on a phone/tablet.

Sonos CR200 update screenBack to the ‘dead spot’ fault. This is where areas of the controller stop responding to touch in some areas. It mostly seems to affect the lower areas of the screen, making it impossible to view the playlist, use the back button etc. When it affects the bottom row, it is impossible to upgrade your controller as it needs you to press ‘Next’ to continue. This renders the controller useless. (If only they had a way of reducing the screen display area to move the buttons, or flipping the screen when you turned the controller upside down.)

 

So, I thought I would see if it was possible to fix this myself. If you have ever taken apart an LCD calculator, it has a connection between the glass LCD display and the circuit board made of conductive rubber which can be cleaned and reassembled. I was hoping for this type of thing..

Inside CR200Disassembly starts with the battery cover and battery. There are two screws which hold the rear cover in place. One has a tamper-proof label over it so Sonos can see if you have been meddling with it.

Undo the two screws and then the white plastic cover can be lifted away at the top and then withdrawn from the metal chassis at the bottom.

 

Inside a Sonos CR200Here is what it looks like inside. There are two membrane cables with zero-insertion-force connectors. The longer one on the bottom right is for the screen, and the one at the bottom left is for the touch.

These both have black levers which when lifted up allow the cables to be removed easily. Once this is done, the circuit board can be released at the top-centre where it is clipped into the plastic and lifted away. There is one remaining connection to the speaker at the bottom. In my case, the speaker came away from the base so it didn’t need undoing.

Touch screen connections inside Sonos CR200Once the circuit board is removed, you have the screen, which now be lifted out. You can now see the touch screen circuit around the edge of the glass and the connections from the glass to the membrane wiring and controller chip.

So, the touch resolution of the screen is really low, and so one row or column failing will render the touch function useless – with a resolution even as low as double this it could cope with a row or column failure. The membrane for the touch is bonded to the screen and so there is nothing further that can be done.

Summary: Other than to marvel at the excellent design of the metal case and internals, there is no point in ever taking a CR200 apart. The dead-spot touch failure is either in the tracks in the glass (unlikely), their connections to the membrane or the controller chip. Given that the failures seem to happen in commonly used areas, it is possibly a design fault in the controller chip (like lack of sufficient over-voltage protection). The glass appears bonded to the metal case and so you can’t even swap it out. What a shame Sonos didn’t invest in fixing this. I still prefer their controllers to an iOS app.