Skip to content

Commit

Permalink
Merge pull request #2 from lains/support-triphase-historical-tic
Browse files Browse the repository at this point in the history
Support triphase historical tic
  • Loading branch information
lains authored Apr 20, 2024
2 parents 051b6c6 + 47ebe69 commit f36eb18
Show file tree
Hide file tree
Showing 18 changed files with 471 additions and 92 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ INCLUDES += $(INCLUDE_DIRS_SIMPLIFIED:%=-I%)
CXXFLAGS = -g -O0 -Wall -Wextra -Warray-bounds -Wno-unused-parameter -fno-exceptions
CXXFLAGS += -mcpu=cortex-m7 -mthumb -mlittle-endian -mthumb-interwork
CXXFLAGS += -mfloat-abi=hard -mfpu=fpv4-sp-d16
CXXFLAGS += -DEMBEDDED_DEBUG_CONSOLE
ifeq ($(TARGET_BOARD),STM32F469I_DISCO)
CXXFLAGS += -DSTM32F469xx -DUSE_STM32469I_DISCOVERY -DUSE_STM32469I_DISCO_REVB -DUSE_HAL_DRIVER # Board specific defines
endif
Expand Down
53 changes: 48 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ On the STM32F769I-DISCO., I use USART6, the above pins are all available on the
* Vcc is on CN11, pin 2
* USART6 RX is on CN13, pin 1 (maps to PC7)

The software is currently configured to decode Linky data in standard TIC mode (9600 bauds), which is not the default built-in mode for Linky meters.
In order to switch to this more verbose mode (that also provides more data), you need to make a request to your energy vendor.
As an alternative, the code can be slightly tweaked to switch to historical mode and to work at 1200 bauds (this is actually the default mode for Linky meters).
In that case, replace 9600 by 1200 in source file [Stm32SerialDriver.cpp](https://github.com/lains/stm32-linky-display/blob/master/src/hal/Stm32SerialDriver.cpp) inside the function called `MX_USART_TIC_UART_Init()`.
The software is currently configured to decode Linky data in historical TIC mode (1200 bauds), which is the default built-in mode for Linky meters.
You can get more data out of your meter by switching to standard TIC mode. In order to switch to this more verbose mode, you need to make a request to your energy vendor.
In that case, replace 1200 by 9600 in source file [main.cpp](https://github.com/lains/stm32-linky-display/blob/master/src/main.cpp) when function `ticSerial.start()` is called.

In order to compile the code, this project uses:
* GNU Make (Build System)
* GNU ARM Embedded Toolchain (Compiler)
* STM32CubeF4 MCU Firmware Package (BSP/Drivers)
* STM32CubeF4 or STM32CubeF7 MCU Firmware Package (BSP/Drivers), depending on the board selected
* [ticdecodecpp](https://github.com/lains/ticdecodecpp) as a C++ library to decode the TIC serial data on the fly
* ST-Link or OpenOCD (Debug)

Expand Down Expand Up @@ -72,6 +71,50 @@ The instantaneous power consumption will be displayed in real-time.
* Optionally, open a serial terminal to view the `printf` function calls.
* For example, run `pyserial`: `python -m serial - 115200` and then select the port labeled "STM32 STLink".

### Debugging console

A debugging console is available using the Stm32DebugOuput class (singleton)
This serial port is wired to the virtual serial port provided by the ST-Link probe, so it's easy to see debugging messages, directly by reading from the ST-Link virtual serial port (something like /dev/ttyACM0 on Linux).

The following code snippet allows to print out debugging data:
```
#include "Stm32DebugOutput.h"
uint8_t buffer[3] = { 0x01, 0x02, 0x03 }
Stm32DebugOutput& debugSerial = Stm32DebugOutput::get();
debugSerial.send("Sample buffer content: ");
debugSerial.hexdumpBuffer(buffer, sizeof(buffer));
debugSerial.send("\n");
```

> **Note**
> In order to enable debug logs on the debugging console in the current code, you should define the following compiler directive `EMBEDDED_DEBUG_CONSOLE`
### Emulating TIC signal

It is possible to directly wire your STM32 TIC USART port to a PC in order to avoid having to connect to a realy Linky meter.

This can be useful to debug a TIC stream (with or without transmission errors), by first collecting the data, and then replaying it to the embedded code at a later stage, with debug on.

You will need a 3.3V TTL-level serial adapter plugged into your PC.
This often creates a virtual serial device on your PC, like `/dev/ttyUSB0` on Linux.

You can now configure this port with the same config as the Linky meter:
```
stty -F /dev/ttyUSB0 1200 sane evenp parenb cs7 -crtscts
```

> **Note**
> 1200 is for historical TIC, use 9600 bauds instead for standard TIC
Now, replay a captured TIC stream, for example:
```
cat ticdecodecpp/test/samples/continuous_linky_3P_historical_TIC_with_rx_errors.bin | pv -L 100 >/dev/ttyUSB0
```

> **Note**
> `pv` allows to give a specific pace for data flow (100 characters par second in the above example)
## Developper guide

### Porting
Expand Down
12 changes: 12 additions & 0 deletions inc/domain/TicFrameParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,20 @@ class TicFrameParser {
protected:
void onNewMeasurementAvailable();

/**
* @brief Take a TIC frame date timestamp into account
*
* @param horodate The orodate data that has been read in the current TIC frame
**/
void onNewDate(const TIC::Horodate& horodate);

/**
* @brief Try to guess the arrival time for the current frame and store it as the current frame's horodate
*
* @note This is required for historical TIC frames that include no date timestamp
**/
void guessFrameArrivalTime();

void onRefPowerInfo(uint32_t power);

void onNewComputedPower(int minValue, int maxValue);
Expand Down
1 change: 1 addition & 0 deletions inc/domain/TicProcessingContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "../hal/Stm32SerialDriver.h"
#else
struct Stm32SerialDriver {
Stm32SerialDriver() {}
};
#endif
#include <stdint.h>
Expand Down
73 changes: 73 additions & 0 deletions inc/hal/Stm32DebugOutput.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#ifndef _STM32DEBUGOUTPUT_H_
#define _STM32DEBUGOUTPUT_H_

#ifdef STM32F469xx
#include "stm32f4xx_hal.h"
#endif
#ifdef STM32F769xx
#include "stm32f7xx_hal.h"
#endif
#include <cstdint>
#ifdef USE_ALLOCATION
#include <string>
#endif
#include "Stm32SerialDriver.h" // FIXME: this is just to get USART_DBG, we should probably move this into another file (main.h?)

/**
* @brief Serial link debug output class (singleton)
*/
class Stm32DebugOutput {
public:
/**
* @brief Singleton instance getter
*
* @return The singleton instance of this class
*/
static Stm32DebugOutput& get();

/** @brief Reset the debug output serial port
*
* @warning It is required to calling this function once before any other method
* The serial port is not initialized at construction, because this class is a singletong and is constructed too early in the program initialization process
*/
void start();

/**
* @brief Send a byte buffer to the debug console
*/
bool send(const uint8_t* buffer, unsigned int len);

/**
* @brief Send a C-formatted string to the debug console
*/
bool send(const char* text);

#ifdef USE_ALLOCATION
/**
* @brief Send a C++ std::string to the debug console
*/
bool send(const std::string& text);
#endif

/**
* @brief Send an unsigned int value to the debug console
*/
bool send(unsigned int value);

/**
* @brief Send the hex dump of a byte buffer to the debug console
*/
bool hexdumpBuffer(const uint8_t* buffer, unsigned int len);

private:
Stm32DebugOutput();

/* Attributes */
private:
static Stm32DebugOutput instance; /*!< Lazy singleton instance */
UART_HandleTypeDef handle; /*!< The UART handle to use for output */
public:
bool inError; /*!< Is the output currently in error? */
};

#endif // _STM32DEBUGOUTPUT_H_
16 changes: 9 additions & 7 deletions inc/hal/Stm32SerialDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@
/* Definition for USART3 HAL functions */
#define USART3_CLK_ENABLE() __HAL_RCC_USART3_CLK_ENABLE()
#define USART3_CLK_DISABLE() __HAL_RCC_USART3_CLK_DISABLE()
#define USART3_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define USART3_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define USART3_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define USART3_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define USART3_FORCE_RESET() __HAL_RCC_USART3_FORCE_RESET()
#define USART3_RELEASE_RESET() __HAL_RCC_USART3_RELEASE_RESET()
#define USART3_TX_PIN GPIO_PIN_8
#define USART3_TX_GPIO_PORT GPIOD
#define USART3_TX_PIN GPIO_PIN_10
#define USART3_TX_GPIO_PORT GPIOB
#define USART3_TX_AF GPIO_AF7_USART3
#define USART3_RX_PIN GPIO_PIN_9
#define USART3_RX_GPIO_PORT GPIOD
#define USART3_RX_PIN GPIO_PIN_11
#define USART3_RX_GPIO_PORT GPIOB
#define USART3_RX_AF GPIO_AF7_USART3

#define USART_DBG USART3
Expand Down Expand Up @@ -138,8 +138,10 @@ class Stm32SerialDriver {

/**
* @brief Initialize the serial link and start receiving data from it
*
* @param baudrate The baudrate to use on the serial port
*/
void start();
void start(uint32_t baudrate);

/**
* @brief Reset the reception buffer overflow counter
Expand Down
18 changes: 12 additions & 6 deletions src/domain/PowerHistory.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "PowerHistory.h"
#ifdef EMBEDDED_DEBUG_CONSOLE
#include "Stm32DebugOutput.h"
#endif

#include <climits>

Expand Down Expand Up @@ -102,12 +105,15 @@ void PowerHistory::onNewPowerData(const TicEvaluatedPower& power, const Timestam
this->ticContext->lastParsedFrameNb = frameSequenceNb;
}

if (this->timestampsAreInSamePeriodSample(timestamp, this->lastPowerTimestamp)) {
PowerHistoryEntry* lastEntry = this->data.getPtrToLast();
if (lastEntry != nullptr) {
lastEntry->averageWithPowerSample(power, timestamp);
this->lastPowerTimestamp = timestamp;
return;
if (timestamp.isValid) {
if (this->timestampsAreInSamePeriodSample(timestamp, this->lastPowerTimestamp)) {
PowerHistoryEntry* lastEntry = this->data.getPtrToLast();
if (lastEntry != nullptr) {
lastEntry->averageWithPowerSample(power, timestamp);
this->lastPowerTimestamp = timestamp;
return;
}
/* If lastEntry is not valid, create a new entry by falling-through the following code */
}
/* If lastEntry is not valid, create a new entry by falling-through the following code */
}
Expand Down
110 changes: 102 additions & 8 deletions src/domain/TicFrameParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#include <string.h>
#include <utility> // For std::swap()

#ifdef EMBEDDED_DEBUG_CONSOLE
#include "Stm32DebugOutput.h"
#endif

TicEvaluatedPower::TicEvaluatedPower() :
isValid(false),
minValue(INT_MIN),
Expand Down Expand Up @@ -204,6 +208,33 @@ void TicFrameParser::onNewDate(const TIC::Horodate& horodate) {
this->lastFrameMeasurements.timestamp = Timestamp(horodate);
}

void TicFrameParser::guessFrameArrivalTime() {
this->onNewMeasurementAvailable();
if (this->lastFrameMeasurements.fromFrameNb != this->nbFramesParsed) {
#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("It seems we are in a new frame ID ");
Stm32DebugOutput::get().send(static_cast<unsigned int>(this->nbFramesParsed));
Stm32DebugOutput::get().send("\n");
#endif
}
unsigned int emulatedSecond = (this->nbFramesParsed * 3) % 60; /* Assume 1 historical TIC frame every 3 seconds */
unsigned int emulatedHorodateRemainder = (this->nbFramesParsed / 20); /* Counts total remainder as minutes */
unsigned int emulatedMinute = emulatedHorodateRemainder % 60;
emulatedHorodateRemainder = emulatedHorodateRemainder / 60; /* Now count total remainder as hours */
unsigned int emulatedHour = emulatedHorodateRemainder % 24;
/* Note: we discard days and month for now */
this->lastFrameMeasurements.timestamp = Timestamp(emulatedHour, emulatedMinute, emulatedSecond);
#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("Injecting timestamp in historical frame: ");
Stm32DebugOutput::get().send(static_cast<unsigned int>(this->lastFrameMeasurements.timestamp.hour));
Stm32DebugOutput::get().send(":");
Stm32DebugOutput::get().send(static_cast<unsigned int>(this->lastFrameMeasurements.timestamp.minute));
Stm32DebugOutput::get().send(":");
Stm32DebugOutput::get().send(static_cast<unsigned int>(this->lastFrameMeasurements.timestamp.second));
Stm32DebugOutput::get().send("\n");
#endif
}

void TicFrameParser::onRefPowerInfo(uint32_t power) {
//FIXME: Todo
}
Expand All @@ -225,6 +256,40 @@ void TicFrameParser::onNewFrameBytes(const uint8_t* buf, unsigned int cnt) {
}

void TicFrameParser::onNewComputedPower(int minValue, int maxValue) {

#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("onNewComputedPower(");
if (minValue != maxValue)
Stm32DebugOutput::get().send("[");
{
if (minValue < 0) {
Stm32DebugOutput::get().send("-");
Stm32DebugOutput::get().send(static_cast<unsigned int>(-minValue));
}
else {
Stm32DebugOutput::get().send(static_cast<unsigned int>(minValue));
}
}
if (minValue != maxValue) {
Stm32DebugOutput::get().send(";");
if (maxValue < 0) {
Stm32DebugOutput::get().send("-");
Stm32DebugOutput::get().send(static_cast<unsigned int>(-maxValue));
}
else {
Stm32DebugOutput::get().send(static_cast<unsigned int>(maxValue));
}
Stm32DebugOutput::get().send("]");
}
Stm32DebugOutput::get().send("W) with ");
if (this->lastFrameMeasurements.timestamp.isValid) {
Stm32DebugOutput::get().send("a valid");
}
else {
Stm32DebugOutput::get().send("no");
}
Stm32DebugOutput::get().send("horodate\n");
#endif
this->lastFrameMeasurements.instPower.setMinMax(minValue, maxValue);
if (this->onNewPowerData != nullptr) {
this->onNewPowerData(this->lastFrameMeasurements.instPower, this->lastFrameMeasurements.timestamp, this->nbFramesParsed, onNewPowerDataContext);
Expand All @@ -238,23 +303,52 @@ void TicFrameParser::onFrameComplete() {

void TicFrameParser::onDatasetExtracted(const uint8_t* buf, unsigned int cnt) {
/* This is our actual parsing of a newly received dataset */
//std::cout << "Entering TicFrameParser::onDatasetExtracted() with a " << std::dec << cnt << " byte(s) long dataset\n";

#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("onDatasetExtracted() called with ");
Stm32DebugOutput::get().send(static_cast<unsigned int>(cnt));
Stm32DebugOutput::get().send(" bytes\n");
#endif
TIC::DatasetView dv(buf, cnt); /* Decode the TIC dataset using a dataset view object */
//std::cout << "Above dataset is " << std::string(dv.isValid()?"":"in") << "valid\n";
if (dv.isValid()) {
#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("New dataset: ");
Stm32DebugOutput::get().send(dv.labelBuffer, dv.labelSz);
Stm32DebugOutput::get().send("\n");
#endif
if (dv.decodedType == TIC::DatasetView::ValidHistorical) { /* In this case, we will have no horodate, evaluate time instead */
#ifdef EMBEDDED_DEBUG_CONSOLE
Stm32DebugOutput::get().send("Dataset above is following historical format\n");
#endif
this->guessFrameArrivalTime();
}
//std::vector<uint8_t> datasetLabel(dv.labelBuffer, dv.labelBuffer+dv.labelSz);
//std::cout << "Dataset has label \"" << std::string(datasetLabel.begin(), datasetLabel.end()) << "\"\n";
if (dv.labelSz == 4 &&
memcmp(dv.labelBuffer, "DATE", 4) == 0) {
/* The current label is a DATE */
if (dv.horodate.isValid) {
this->onNewDate(dv.horodate);
}
}
/* Search for SINSTS */
else if (dv.labelSz == 6 &&
memcmp(dv.labelBuffer, "SINSTS", 6) == 0 &&
dv.dataSz > 0) {
/* The current label is a SINSTS with some value associated */
uint32_t sinsts = dv.uint32FromValueBuffer(dv.dataBuffer, dv.dataSz);
if (sinsts != (uint32_t)-1)
this->onNewWithdrawnPowerMesurement(sinsts);
/* Search for SINSTS or PAPP */
else if ( (dv.labelSz == 6 &&
memcmp(dv.labelBuffer, "SINSTS", 6) == 0) ||
(dv.labelSz == 4 &&
memcmp(dv.labelBuffer, "PAPP", 4) == 0)
) {
//std::cout << "Found inst power dataset\n";
if (dv.dataSz > 0) {
/* The current label is a SINSTS or PAPP with some value associated */
//std::vector<uint8_t> datasetValue(dv.dataBuffer, dv.dataBuffer+dv.dataSz);
//std::cout << "Power data received: \"" << std::string(datasetValue.begin(), datasetValue.end()) << "\"\n";
uint32_t withdrawnPower = dv.uint32FromValueBuffer(dv.dataBuffer, dv.dataSz);
//std::cout << "interpreted as " << withdrawnPower << "W\n";
if (withdrawnPower != (uint32_t)-1)
this->onNewWithdrawnPowerMesurement(withdrawnPower);
}
}
/* Search for URMS1 */
else if (dv.labelSz == 5 &&
Expand Down
Loading

0 comments on commit f36eb18

Please sign in to comment.