Todd Peterson
Created November 28, 2019 © GPL3+

Outback MX60 Solar Manager Monitor

This system monitors solar panel voltage and current and battery charger voltage and current. This provides insight to system health.

IntermediateFull instructions provided6 hours39
Outback MX60 Solar Manager Monitor

Things used in this project

Hardware components

RS-232 to 3.3V translator (Sparkfun PRT-00449)
This is required to communicate with the Outback Mate.
×1
Outback Mate
This is the interface to the MX60 solar panel controllers and provides the RS-232 data stream.
×1
SparkFun Clamp-On Current Tap
This is used to measure demand current to the facility. Any suitable CT may be used. The choice of the CT depends on maximum AC current, diameter of the supply line, output characteristics (AC or DC output). If the CT has an AC output, the signal must be converted to DC using a full-wave bridge rectifier and possibly scaled to the ADC range of 2.5V.
×1
Outback MX60
These are the charge controllers. Two are installed at the test site.
×1
Outback Power Communications Hub
This is required to interface the MX60 units to the Mate display.
×1
Azure Sphere MT3620 Starter Kit
Avnet Azure Sphere MT3620 Starter Kit
Of course!
×1

Software apps and online services

Microsoft Azure
Microsoft Azure
Visual Studio 2017
Microsoft Visual Studio 2017

Story

Read more

Custom parts and enclosures

Bench Prototyping System

This is the system on the bench before deployment. The enclosure for the system is an off-the-shelf Hammond box.

Schematics

Azure Sphere Markup

Connections used by the MX60 monitoring system.

Code

MX60 C MX60 Parsing Code

C/C++
This parses the RS-232 output of the Outback Mate, saves to a file and places in shared memory for use by a web server or other mechanism like MQTT. This code resides on a server that receives the data from the Azure Sphere device over the network. Currently, the Azure Sphere sends the data via a UDP socket.
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/neutrino.h>
#include <sys/time.h>
#include <hw/inout.h>
#include <signal.h>
#include <curl/curl.h>

#include "mx60.h"

#define HS_ADC_SHMEM_NAME "/mx60"
#define PROGNAME "mx60-recorder"

mx60_t* mx60_buf;
static int keep_running = 1;
FILE* out;

void create_shmem()
{
    int shmem_fd;
    int amount = 3 * sizeof(mx60_t);
    shmem_fd = shm_open( HS_ADC_SHMEM_NAME, O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);
    if ( shmem_fd == -1 ) {
        fprintf(stderr, "%s: error creating the shared memory object '%s': %s\n",
                PROGNAME, HS_ADC_SHMEM_NAME, strerror(errno) );
        exit( EXIT_FAILURE );
    }
    ftruncate( shmem_fd, amount );
    mx60_buf = (mx60_t*) mmap( 0, amount, PROT_READ|PROT_WRITE|PROT_NOCACHE, MAP_SHARED, shmem_fd, 0 );
    close( shmem_fd );
}

/* to break us out of character handler loop */
void killHandler(int signo) {
    if (signo == SIGINT || signo == SIGTERM) {
        keep_running = 0;
    }
}

const char* default_in_filename = "/dev/ser2";
const char* default_out_filename = "test.csv";

int main(int argc, char *argv[]) {
	int out_fd;
	int in_fd;
	char* out_filename = (char*) default_out_filename;
	char* in_filename = (char*) default_in_filename;
	int n;
	int flags;
	int position;
	int channel;
	char buf[128];
	char* endptr;
	char opt;
	struct timeval tv;

	while ((opt = getopt(argc, argv, "i:o:")) != -1) {
    	switch(opt) {
    	case 'i': // input filename (port)
    		in_filename = optarg;
    		break;
    	case 'o': // output filename
    		out_filename = optarg;
    		break;
    	}
    }

	in_fd = open(in_filename, O_RDONLY, NULL);
	if (in_fd <= 0) {
		printf("could not open %s\n", in_filename);
		return EXIT_FAILURE;
	}

	out_fd = open(out_filename, O_CREAT | O_TRUNC | O_WRONLY | O_SYNC ,
			S_IRUSR | S_IRGRP | S_IROTH);
	if (out_fd <= 0) {
		printf("could not open %s\n", out_filename);
		return 1;
	}

	out = fopen("/dev/null", "w");
	// flush the port
	flags = fcntl(in_fd, F_GETFL, 0);
	fcntl(in_fd, F_SETFL, flags | O_NONBLOCK);
	do {
		n = read(in_fd, &opt, 1);
	} while (n != -1);
	fcntl(in_fd, F_SETFL, flags & ~(O_NONBLOCK));

	signal(SIGINT, killHandler);
	signal(SIGTERM, killHandler);
	create_shmem();

	while (keep_running) {
		do {
			read(in_fd, &opt, 1);
		} while (opt != 10);
		read(in_fd, &opt, 1);
		if (opt == 'A') {
			channel = 0;
		} else if (opt == 'B') {
			channel = 1;
		} else if (opt == 'C') {
			channel = 2;
		} else {
			printf("unexpected channel %d\n", opt);
			continue;
		}
		gettimeofday(&tv, NULL);
		sprintf(buf, "%d.%06d,%c,", tv.tv_sec, tv.tv_usec, opt);
		write(out_fd, buf, strlen(buf));
		read(in_fd, &opt, 1); // skip comma
		position = 0;
		do {
			read(in_fd, &buf[position], 1);
		} while (buf[position++] != 10);
		buf[position-1] = '\n';
		buf[position] = 0;
		printf("%d %d.%06d %s", channel, tv.tv_sec, tv.tv_usec, buf);
		write(out_fd, buf, strlen(buf));

		strtol(buf, &endptr, 0); // skip unused field
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].charger_current = strtol(endptr, &endptr, 10);
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].pv_current = strtol(endptr, &endptr, 10);
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].pv_voltage = strtol(endptr, &endptr, 10);
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].daily_kwh = strtol(endptr, &endptr, 10) / 10.0;
		endptr += 4; // skip comma, 0, comma
		strtol(endptr, &endptr, 0); // skip aux mode
		endptr++;
		if (endptr[0] == '0') endptr++; // skip leading zero
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].error_mode = strtol(endptr, &endptr, 10);
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].charger_mode = strtol(endptr, &endptr, 10);
		endptr++; // skip comma
		if (endptr[0] == '0') endptr++; // skip leading zero
		if (endptr[0] == '0') endptr++; // skip leading zero
		mx60_buf[channel].battery_voltage = strtol(endptr, &endptr, 10) / 10.0;
		mx60_buf[channel].pv_power = mx60_buf[channel].pv_current * mx60_buf[channel].pv_voltage;
	}
	close(in_fd);
	close(out_fd);
	return EXIT_SUCCESS;
}

MX60 header file

C Header File
Contains a data structure that conforms to the Outback Mate RS-232 output format.
/*
 * mx60.h
 *
 *      Author: tpeterson
 */

#ifndef MX60_H_
#define MX60_H_

typedef struct mx60_s {
	float timestamp;
	int charger_current;
	int pv_current;
	int pv_voltage;
	float daily_kwh;
	int error_mode;
	int charger_mode;
	float battery_voltage;
	int pv_power;
} mx60_t;


#endif /* MX60_H_ */

Visual Studio C code

C/C++
This code interacts with the UART, ADC and network. The code reads the ADC at 1 Hz and sends to the server via UDP messages. Only 1 ADC is currently supported by the libraries. Hopefully, this will be remedied soon. MX60 data is read from the UART. The data is sent to a server using a UDP socket. The server parses the ADC and MX60 data and presents to the user via a web interface.
/* Copyright (c) Microsoft Corporation. All rights reserved.
   Licensed under the MIT License. */

// This sample C application for Azure Sphere demonstrates how to use a UART (serial port),
// an ADC and the Wifi interface.
//
// It uses the API for the following Azure Sphere application libraries:
// - UART (serial port)
// - ADC (analog to digital converter)
// - Wifi
// - log (messages shown in Visual Studio's Device Output window during debugging)

#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h> 
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <stdio.h>

// applibs_versions.h defines the API struct versions to use for applibs APIs.
#include "applibs_versions.h"
#include "epoll_timerfd_utilities.h"
#include <applibs/uart.h>
#include <applibs/log.h>
#include <applibs/adc.h>
#include <applibs/networking.h>

// By default, this sample is targeted at the MT3620 Reference Development Board (RDB).
// This can be changed using the project property "Target Hardware Definition Directory".
// This #include imports the sample_hardware abstraction from that hardware definition.
#include <hw/sample_hardware.h>

// File descriptors - initialized to invalid value
static int uartFd = -1;
static int epollFd = -1;
static int pollTimerFd = -1;
static int adcControllerFd = -1;

// The size of a sample in bits
static int sampleBitCount = -1;

// The maximum voltage
static float sampleMaxVoltage = 2.5f;

// Termination state
static volatile sig_atomic_t terminationRequired = false;

/// <summary>
///     Signal handler for termination requests. This handler must be async-signal-safe.
/// </summary>
static void TerminationHandler(int signalNumber)
{
    // Don't use Log_Debug here, as it is not guaranteed to be async-signal-safe.
    terminationRequired = true;
}

// send data to a server over a UDP socket
static int sendData(char* content) {
	const char* hostname = "xxx.xxx.xxx.xxx"; // replace with your server IP address
	const char* portname = "9090";
	struct addrinfo hints;
	bool isNetworkingReady = false;
	if ((Networking_IsNetworkingReady(&isNetworkingReady) < 0) || !isNetworkingReady) {
		Log_Debug("\nNot doing download because there is no internet connectivity.\n");
		return -1;
	}
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = 0;
	hints.ai_flags = AI_ADDRCONFIG;
	struct addrinfo* res = 0;
	int err = getaddrinfo(hostname, portname, &hints, &res);
	if (err != 0) {
		Log_Debug("failed to resolve remote socket address (err=%d)", err);
		return err;
	}
	int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	if (fd == -1) {
		Log_Debug("%s", strerror(errno));
		goto sendErr;
	}
	if (sendto(fd, content, sizeof(content), 0,
		  res->ai_addr, res->ai_addrlen) == -1) {
		Log_Debug("%s", strerror(errno));
	}
sendErr:
	freeaddrinfo(res);
	return 0;
}

/// <summary>
///     Handle polling timer event: takes a single reading from ADC channelId,
///     every second, outputting the result.
/// </summary>
static void AdcPollingEventHandler(EventData *eventData)
{
	if (ConsumeTimerFdEvent(pollTimerFd) != 0) {
		terminationRequired = true;
		return;
	}

	static char buffer[64];
	uint32_t value;
	int result = ADC_Poll(adcControllerFd, SAMPLE_POTENTIOMETER_ADC_CHANNEL, &value);
	if (result < -1) {
		Log_Debug("ADC_Poll failed with error: %s (%d)\n", strerror(errno), errno);
		terminationRequired = true;
		return;
	}

	float voltage = ((float)value * sampleMaxVoltage) / (float)((1 << sampleBitCount) - 1);
	sprintf(buffer, "%6.4f", value);
	sendData(buffer);
	Log_Debug("The out sample value is %.3f V\n", voltage);
}

// event handler data structures. Only the event handler field needs to be populated.
static EventData adcPollingEventData = { .eventHandler = &AdcPollingEventHandler };

/// <summary>
///     Handle UART event: if there is incoming data, process it.
/// </summary>
static void UartEventHandler(EventData *eventData)
{
    const size_t receiveBufferSize = 256;
    uint8_t receiveBuffer[receiveBufferSize + 1]; // allow extra byte for string termination
    ssize_t bytesRead;

    // Read incoming UART data. It is expected behavior that messages may be received in multiple
    // partial chunks.
    bytesRead = read(uartFd, receiveBuffer, receiveBufferSize);
    if (bytesRead < 0) {
        Log_Debug("ERROR: Could not read UART: %s (%d).\n", strerror(errno), errno);
        terminationRequired = true;
        return;
    }

    if (bytesRead > 0) {
        // Null terminate the buffer to make it a valid string, and print it
        receiveBuffer[bytesRead] = 0;
		sendData(receiveBuffer);
        Log_Debug("UART received %d bytes: '%s'.\n", bytesRead, (char *)receiveBuffer);
    }
}

static EventData uartEventData = {.eventHandler = &UartEventHandler};

/// <summary>
///     Set up SIGTERM termination handler, initialize peripherals, and set up event handlers.
/// </summary>
/// <returns>0 on success, or -1 on failure</returns>
static int InitPeripheralsAndHandlers(void)
{
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_handler = TerminationHandler;
    sigaction(SIGTERM, &action, NULL);

    epollFd = CreateEpollFd();
    if (epollFd < 0) {
        return -1;
    }

    // Create a UART_Config object, open the UART and set up UART event handler
    UART_Config uartConfig;
    UART_InitConfig(&uartConfig);
    uartConfig.baudRate = 115200;
    uartConfig.flowControl = UART_FlowControl_None;
    uartFd = UART_Open(SAMPLE_UART, &uartConfig);
    if (uartFd < 0) {
        Log_Debug("ERROR: Could not open UART: %s (%d).\n", strerror(errno), errno);
        return -1;
    }
    if (RegisterEventHandlerToEpoll(epollFd, uartFd, &uartEventData, EPOLLIN) != 0) {
        return -1;
    }

 	adcControllerFd = ADC_Open(SAMPLE_POTENTIOMETER_ADC_CONTROLLER);
	if (adcControllerFd < 0) {
		Log_Debug("ADC_Open failed with error: %s (%d)\n", strerror(errno), errno);
		return -1;
	}

	sampleBitCount = ADC_GetSampleBitCount(adcControllerFd, SAMPLE_POTENTIOMETER_ADC_CHANNEL);
	if (sampleBitCount == -1) {
		Log_Debug("ADC_GetSampleBitCount failed with error : %s (%d)\n", strerror(errno), errno);
		return -1;
	}
	if (sampleBitCount == 0) {
		Log_Debug("ADC_GetSampleBitCount returned sample size of 0 bits.\n");
		return -1;
	}

	int result = ADC_SetReferenceVoltage(adcControllerFd, SAMPLE_POTENTIOMETER_ADC_CHANNEL,
		sampleMaxVoltage);
	if (result < 0) {
		Log_Debug("ADC_SetReferenceVoltage failed with error : %s (%d)\n", strerror(errno), errno);
		return -1;
	}

	struct timespec adcCheckPeriod = { .tv_sec = 1,.tv_nsec = 0 };
	pollTimerFd =
		CreateTimerFdAndAddToEpoll(epollFd, &adcCheckPeriod, &adcPollingEventData, EPOLLIN);
	if (pollTimerFd < 0) {
		return -1;
	}

	return 0;
}

/// <summary>
///     Close peripherals and handlers.
/// </summary>
static void ClosePeripheralsAndHandlers(void)
{
    Log_Debug("Closing file descriptors.\n");
    CloseFdAndPrintError(uartFd, "Uart");
    CloseFdAndPrintError(epollFd, "Epoll");
	CloseFdAndPrintError(pollTimerFd, "Timer");
	CloseFdAndPrintError(adcControllerFd, "ADC");
}

/// <summary>
///     Main entry point for this application.
/// </summary>
int main(int argc, char *argv[])
{
    Log_Debug("MX60 application starting.\n");
    if (InitPeripheralsAndHandlers() != 0) {
        terminationRequired = true;
    }

    // Use epoll to wait for events and trigger handlers, until an error or SIGTERM happens
    while (!terminationRequired) {
        if (WaitForEventAndCallHandler(epollFd) != 0) {
            terminationRequired = true;
        }
    }

    ClosePeripheralsAndHandlers();
    Log_Debug("Application exiting.\n");
    return 0;
}

Application Manifest

JSON
This provides the definitions necessary to access the hardware properly.
{
  "SchemaVersion": 1,
  "Name" : "UART_HighLevelApp",
  "ComponentId" : "4f2d9823-dbbd-4740-a7dd-198c32ba34fe",
  "EntryPoint": "/bin/app",
  "CmdArgs": [],
  "Capabilities": {
    "Uart": [ "$SAMPLE_UART" ],
    "Adc": [
      "$SAMPLE_POTENTIOMETER_ADC_CONTROLLER"
    ],
	"AllowedConnections": [ "my-server.com" ]
  },
  "ApplicationType": "Default"
}

Visual Studio Project File

Properties
This defines the project settings. The important item to note is " <TargetSysroot>3+Beta1909</TargetSysroot>".
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|ARM">
      <Configuration>Debug</Configuration>
      <Platform>ARM</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|ARM">
      <Configuration>Release</Configuration>
      <Platform>ARM</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{1a0db47c-ab49-4b10-985f-3feccc6526f3}</ProjectGuid>
    <Keyword>AzureSphere</Keyword>
    <RootNamespace>UART</RootNamespace>
    <MinimumVisualStudioVersion>15.0</MinimumVisualStudioVersion>
    <ApplicationType>Linux</ApplicationType>
    <ApplicationTypeRevision>1.0</ApplicationTypeRevision>
    <TargetLinuxPlatform>Generic</TargetLinuxPlatform>
    <LinuxProjectType>{D51BCBC9-82E9-4017-911E-C93873C4EA2B}</LinuxProjectType>
    <DebugMachineType>Device</DebugMachineType>
    <PlatformToolset>GCC_AzureSphere_1_0</PlatformToolset>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'" Label="Configuration">
    <UseDebugLibraries>true</UseDebugLibraries>
    <TargetSysroot>3+Beta1909</TargetSysroot>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'" Label="Configuration">
    <UseDebugLibraries>false</UseDebugLibraries>
    <TargetSysroot>2</TargetSysroot>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings" />
  <ImportGroup Label="Shared" />
  <ImportGroup Label="PropertySheets" />
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup>
    <TargetHardwareDirectory>..\..\..\Hardware\mt3620_rdb</TargetHardwareDirectory>
    <TargetHardwareDefinition>sample_hardware.json</TargetHardwareDefinition>
  </PropertyGroup>
  <ItemGroup>
    <ClCompile Include="main.c" />
    <ClCompile Include="epoll_timerfd_utilities.c" />
    <ClInclude Include="epoll_timerfd_utilities.h" />
    <UpToDateCheckInput Include="app_manifest.json" />
    <ClInclude Include="applibs_versions.h" />
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets" />
  <ItemDefinitionGroup>
    <Link>
      <LibraryDependencies>applibs;pthread;gcc_s;c</LibraryDependencies>
      <AdditionalOptions>-Wl,--no-undefined -nodefaultlibs %(AdditionalOptions)</AdditionalOptions>
    </Link>
    <ClCompile>
      <AdditionalOptions>-Werror=implicit-function-declaration %(AdditionalOptions)</AdditionalOptions>
      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">D:\development\azure-sphere\azure-sphere-samples\Hardware\mt3620_rdb\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
    </ClCompile>
  </ItemDefinitionGroup>
</Project>

Credits

Todd Peterson

Todd Peterson

8 projects • 5 followers
Specialist in real-time embedded systems hardware, software and systems integration. Well rounded in many engineering disciplines.

Comments