Skip to content

DanielMartensson/GoobySoft

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GoobySoft

This is a measuring and controlling software. The purpose of this software is to provide an open source measurement software and control software that is easy to maintain and easy to use. The unique thing with this project is that this project is written so anybody can implement their own device.

Features

  • CANopen/Open SAE J1939/Modbus RTU/Modbus TCP/USB support
  • Measure digital/analog inputs
  • Control output actuators
  • Easy to add own device by using callback functions
  • Database connection
  • Real time data acquisition
  • Multifunctional plots
  • CAN Traffic reader
  • Multiple connections to multiple devices at the same time during data acquisition
  • Latest modern C++ standard

Supported devices

How to add a new device

Assume that you have for example a Modbus or a special USB dongle that can communicate with an input/output device, and you want GoobySoft to read, write and store measurement data and also have a control connection to that device. All you need to do, is to work inside Devices folder of this project.

  1. Begin first to add your new protocol and new device here. Notice the name Tools_Communications_Devices_createDevices(). The project is structured so each folder name begins with a capital letter and subfolders path are displayed with _ and functions begins with lower case letters. So to find Tools_Communications_Devices_createDevices(), head over to Tools/Communications/Devices and open the file Devices.cpp. Each folder have the same folder name as the header and source file. So inside folder Devices, it exists Devices.cpp and Devices.h.
void Tools_Communications_Devices_createDevices() {
	// Get the parameter holder
	Protocol* protocols = Tools_Hardware_ParameterStore_getParameterHolder()->protocols;

	// Reset all
	for (int i = 0; i < MAX_PROTOCOLS; i++) {
		protocols[i].isProtocolUsed = false;
	}

	// Create devices for protocols 
	createProtocolTool(&protocols[0], PROTOCOL_STRING[USB_PROTOCOL_ENUM_MODBUS_RTU], 1); // Modbus RTU, 1 device
	createProtocolTool(&protocols[1], PROTOCOL_STRING[USB_PROTOCOL_ENUM_RAW_USB], 1); protocol), // 1 device
	// Add new protocol here...

	// Create devices
	createDeviceTool(&protocols[0].devices[0], "ADL400", Tools_Communications_Devices_ADL400_getFunctionValues, Tools_Communications_Devices_ADL400_getTableColumnIDs, Tools_Communications_Devices_ADL400_getInput, Tools_Communications_Devices_ADL400_setOutput, Tools_Communications_Devices_ADL400_getColumnFunction);
	createDeviceTool(&protocols[1].devices[0], "STM32 PLC", Tools_Communications_Devices_STM32PLC_getFunctionValues, Tools_Communications_Devices_STM32PLC_getTableColumnIDs, Tools_Communications_Devices_STM32PLC_getInput, Tools_Communications_Devices_STM32PLC_setOutput, Tools_Communications_Devices_STM32PLC_getColumnFunction);
	// Add new device here...
}
  1. Create the getFunctionValues() callback. This function should return a string of function values with \0 as null termination e.g Read Input A\0Read Input B\0Write Output C\0. The reason for that is that ImGui::Combo box want an argument that contains a const char* that null terminations
std::string Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getFunctionValues(){
	std::string functionNames;
	functionNames += "Read Input A";
	functionNames += '\0';
	functionNames += "Read Input B";
	functionNames += '\0';
	functionNames += "Write Output C";
	functionNames += '\0';
	return functionNames;
}
  1. Create the getTableColumnsID() callback. Here you can determine the name of your column when you are going to configure your e.g measurement device or CAN-bus device. Here are some examples below. You don't need to use them all, but some of them are mandatory.
std::vector<TableColumnID> Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getTableColumnIDs() {
	/* 
         * This can:
         * Measure analog/digital inputs, control analog outputs, control analog outputs via e.g CAN-bus field, measure analog/digital inputs via e.g CAN-bus field
         */
	// Only one column definition is allowed.
	std::vector<TableColumnID> tableColumnIDs;
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Port", COLUMN_DEFINITION_PORT)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Function", COLUMN_DEFINITION_FUNCTION)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("CAN address", COLUMN_DEFINITION_ADDRESS));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value raw", COLUMN_DEFINITION_MIN_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value raw", COLUMN_DEFINITION_MAX_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value real", COLUMN_DEFINITION_MIN_VALUE_REAL));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value real", COLUMN_DEFINITION_MAX_VALUE_REAL));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Display name", COLUMN_DEFINITION_DISPLAY_NAME)); // Mandatory field
	return tableColumnIDs;

	/* 
         * This can:
         * Measure analog/digital inputs, control analog outputs
         */
	// Only one column definition is allowed.
	std::vector<TableColumnID> tableColumnIDs;
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Port", COLUMN_DEFINITION_PORT)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Function", COLUMN_DEFINITION_FUNCTION)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value raw", COLUMN_DEFINITION_MIN_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value raw", COLUMN_DEFINITION_MAX_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value real", COLUMN_DEFINITION_MIN_VALUE_REAL));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value real", COLUMN_DEFINITION_MAX_VALUE_REAL));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Display name", COLUMN_DEFINITION_DISPLAY_NAME)); // Mandatory field
	return tableColumnIDs;

	/* 
         * This can:
         * Control analog outputs
         */
	// Only one column definition is allowed.
	std::vector<TableColumnID> tableColumnIDs;
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Port", COLUMN_DEFINITION_PORT)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Function", COLUMN_DEFINITION_FUNCTION)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value raw", COLUMN_DEFINITION_MIN_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value raw", COLUMN_DEFINITION_MAX_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Display name", COLUMN_DEFINITION_DISPLAY_NAME)); // Mandatory field
	return tableColumnIDs;

	/* 
         * This can:
         * Measure analog/digital inputs via e.g CAN-bus field
         */
	// Only one column definition is allowed.
	std::vector<TableColumnID> tableColumnIDs;
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Port", COLUMN_DEFINITION_PORT)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Function", COLUMN_DEFINITION_FUNCTION)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("CAN/Modbus address", COLUMN_DEFINITION_ADDRESS));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Display name", COLUMN_DEFINITION_DISPLAY_NAME)); // Mandatory field
	return tableColumnIDs;

	/* 
         * This can:
         * Control analog outputs via e.g CAN-bus field, measure analog/digital inputs via e.g CAN-bus field
         */
	// Only one column definition is allowed.
	std::vector<TableColumnID> tableColumnIDs;
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Port", COLUMN_DEFINITION_PORT)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Function", COLUMN_DEFINITION_FUNCTION)); // Mandatory field
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("CAN/Modbus address", COLUMN_DEFINITION_ADDRESS));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Min value raw", COLUMN_DEFINITION_MIN_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Max value raw", COLUMN_DEFINITION_MAX_VALUE_RAW));
	tableColumnIDs.emplace_back(Tools_Communications_Devices_createTableIDs("Display name", COLUMN_DEFINITION_DISPLAY_NAME)); // Mandatory field
	return tableColumnIDs;

}

If you have this setup above, then your configuration table is going to look like this. Depending on which function you are selecting, some input fields are hidden. The COLUMN_DEFINITION enum can be found in Parameters.h file

a

  1. Create the getInput() callback. This function want to have three arguments. A C-string port that describe the USB port, or it can also be the Ip Address if Modbus TCP is used. Next argument is the functionValueIndex. That index value corresponds to the index of getFunctionValues() callback. What getInput() does, is that it's reading the measurements of a device and return it back
float Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getInput(const char port[], int functionValueIndex, int address) {
	/* These must follow the same linear pattern as getFunctionValues() */
	setSlaveAddress(port, address); // set slave address if you are using e.g Modbus etc. If not, then you can remove this line
	switch (functionValueIndex) {
	case IO_READ_INPUT_A:
		return functionThatReadsInputA(port);
	case IO_READ_INPUT_B:
		return functionThatReadsInputB(port);
	default:
		return -1.0f;
	}
}
  1. Create the IO enum. This enum is going to serve the argument functionValueIndex together with a switch-statement.
/* These must follow the same linear pattern as getFunctionValues() */
typedef enum {
	IO_READ_INPUT_A,
	IO_READ_INPUT_B,
	IO_WRITE_OUTPUT_C
}IO;
  1. Create the setOutput() callback. Four arguments, same as getInput() callback, but this one have an integer value that GoobySoft is sending to the device for e.g PWM control.
bool Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_setOutput(const char port[], int functionValueIndex, int address, int value) {
	/* These must follow the same linear pattern as getFunctionValues() */
	switch (functionValueIndex) {
	case IO_WRITE_OUTPUT_C:
		// If you are using a device who wants an address, you might consider to include `address` argument inside the funftion
		return funtionThatWritesOutputC(port, value); 
	default:
		return false; // Fail
	}
}
  1. Create the getColumnFunction() callback. Here you are going to select which IO that has a specific purpose. Some functions might be a CAN-bus output or input or some functions might be a pure analog input with 16-bit ADC e.g STM32. The COLUMN_FUNCTION enum can be found in Parameters.h file
COLUMN_FUNCTION Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getColumnFunction(int functionValueIndex) {
	/* These must follow the same linear pattern as getFunctionValues() */
	switch (functionValueIndex) {
	case IO_READ_INPUT_A:
		return COLUMN_FUNCTION_INPUT_SENSOR_ADDRESS; // E.g STM32 with CAN-bus sensor
	case IO_READ_INPUT_B:
		return COLUMN_FUNCTION_INPUT_SENSOR_ANALOG; // E.g Arduino ADC
	case IO_WRITE_OUTPUT_C:
		return COLUMN_FUNCTION_OUTPUT_ACTUATOR_ADDRESS; // E.g PWM CAN-bus unit
	default:
		return COLUMN_FUNCTION_HOLD_DATA; // Just hold data
	}
}

It's very important to select right COLUMN_FUNCTION for a specific IO index. The getColumnFunction() callback determine how your configuration window will look like and if your sensor is going to be calibrated or not.

  1. Now when you have made your protocol for your device, it's time to connect them to GoobySoft
/* 
 * One protocol can contains multiple devices.
 * PROTOCOL_STRING[USB_PROTOCOL_ENUM_RAW_USB] stands for USB communications device class e.g regular USB communication
 * Here I say that `Raw USB` can hold 10 devices.
 */
createProtocolTool(&protocols[1], PROTOCOL_STRING[USB_PROTOCOL_ENUM_RAW_USB], 10); 


/* Add your device to that protocol `Raw USB`. Here I say that my device is `Raw USB` and the device is at index 0 */
createDeviceTool(&protocols[1].devices[0], "<NAME_OF_YOUR_DEVICE>", 
Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getFunctionValues, 
Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getTableColumnIDs, 
Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getInput, 
Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_setOutput, 
Tools_Communications_Devices_<NAME_OF_YOUR_DEVICE>_getColumnFunction);

Now you are done! For examples, head over to Devices folder.

If you got some issues with the combo boxes e.g for Functions or want to add more devices, check out the configuration inside Parameters.h file

#define MAX_PROTOCOLS 5			// How many protocols can be used, Raw USB, Modbus RTU, Modbus TCP etc..
#define MAX_DEVICES 10			// How many devices per protocol
#define MAX_ROWS 10			// How many rows per device inside configuration window
#define MAX_COLUMNS 10			// Max columns for each device inside configuration column
#define MAX_USB_PORTS 10		// Max USB ports that can be connected at the same time
#define MAX_C_STRING_LEN 30		// Max char* length for e.g port, device name etc.
#define MAX_C_STRING_EXTRA_LEN 1024	// Max char* length for function values
#define MAX_DATA_MEASUREMENT_PLOT 1024	// Max plot length for real time measuring

How to install on Windows/Linux

I recommend to use Visual Studio Code (with CMake tools, C/C++ Extensions and Vcpkg CMake Tools) to build this CMake project.

# Clone
git clone https://github.com/microsoft/vcpkg.git
git clone https://github.com/DanielMartensson/GoobySoft.git
git submodule update --init --recursive

# Install 
cd vcpkg
./bootstrap-vcpkg
./vcpkg install mysql-connector-cpp boost-filesystem sdl3 pthreads boost-system opengl opencv cserialport intel-mkl boost-date-time openssl lz4 zstd
cd ..

# Tool chain
cd GoobySoft
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=path/to/vcpkg/scripts/buildsystems/vcpkg.cmake

# Or if you got some issues with MKL or MySQL
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=../vcpkg/scripts/buildsystems/vcpkg.cmake  -DCMAKE_PREFIX_PATH="C:/path/to/../../vcpkg/<vcpkg_ or not>installed/x64-<windows or linux>"

# Build
cmake --build build

CAN <---> USB module

In order to let 'GoobySoft' communicate over CAN through the USB. A CAN to USB module need to be used. You can build it by your self as long as you follow the data frames.

When reading CAN -> USB, the read data frame must be:

Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Byte 7 Byte 8 Byte 9 Byte 10 Byte 11 Byte 12 Byte 13 Byte 14 Byte 15 Byte 16 Byte 17 Byte 18 Byte 19
STD/EXT ID MSB ID ID ID LSB DLC DATA 0 DATA 1 DATA 2 DATA 3 DATA 4 DATA 5 DATA 6 DATA 7 'S' 'T' 'M' '3' '2' '\0'

When writing USB -> CAN, the write data fram must be:

Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Byte 7 Byte 8 Byte 9 Byte 10 Byte 11 Byte 12 Byte 13
STD/EXT ID MSB ID ID ID LSB DLC DATA 0 DATA 1 DATA 2 DATA 3 DATA 4 DATA 5 DATA 6 DATA 7

The total read data frame is 20 bytes in total. The total write data frame is 14 bytes in total. The CAN type STD/EXT can be 1 (STD) or 2 (EXT). This is very important for the SAE J1939 and CANopen protocols. The ending STM32\0 is just for GoobySoft to know the end of the USB message.

I recommend this CAN to USB module: STM32 CAN to USB

Pictures

SAE J1939 Identifications

a

USB connection

a

Database connection

a

View measurements

a

Measuring process

a

File dialog

a

About

General software for logging and controlling devices. Controlling Modbus, CAN bus via STM32PLC etc

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published