Added initial support for HID DMM

This commit is contained in:
nedko 2024-03-05 18:10:55 +02:00
parent 426976cbde
commit 023b9a3100
9 changed files with 912 additions and 2 deletions

2
98-hidraw.rules Normal file
View File

@ -0,0 +1,2 @@
#Allow all to read hidraws
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev"

448
DMM_HID.cpp Normal file
View File

@ -0,0 +1,448 @@
#include "DMM_HID.h"
#include <linux/hidraw.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <libudev.h>
#include <stdint.h>
#include <poll.h>
#include <limits>
#define VENDOR_ID 0x2571
#define PRODUCT_ID 0x4100
using std::vector;
using std::string;
constexpr int DMM_BUFSIZE = 8;
// Checks if a hidraw device matches the vendor id and product id
// If *_id is < 0 - it is not checked
bool check_hidraw_device(string dev, int vendor_id, int product_id)
{
int fd;
int res;
struct hidraw_devinfo info;
bool result = false;
bool vendor = true;
bool product = true;
fd = open(dev.c_str(), O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
return false;
}
memset(&info, 0, sizeof(info));
// Get Raw Info
res = ioctl(fd, HIDIOCGRAWINFO, &info);
if (res >= 0)
{
if ((vendor_id > 0) && (vendor_id != info.vendor))
{
vendor = false;
}
if ((product_id > 0) && (product_id != info.product))
{
product = false;
}
if (vendor && product)
{
result = true;
}
}
close(fd);
return result;
}
// Reads exact number of bytes from a fd for X milisec
bool read_exact(int fd, uint8_t *buf, int size, int timeout_ms)
{
struct pollfd poll_fd;
int bytes;
int bytes_read;
if ((fd < 0) || (nullptr == buf) || (size <= 0) || (timeout_ms <= 0))
{
return false;
}
poll_fd.fd = fd;
poll_fd.events = POLLIN;
bytes_read = 0;
while (bytes_read < size)
{
bytes = poll(&poll_fd, 1, timeout_ms);
if (bytes < 1)
{
return false;
}
bytes = read(fd, buf + bytes_read, size - bytes_read);
if (bytes < 1)
{
return false;
}
bytes_read += bytes;
}
return true;
}
vector<string> DMM_HID::get_dmm_devices()
{
vector<string> result;
struct udev *udev_handle = nullptr;
struct udev_enumerate *udev_enum_handle = nullptr;
struct udev_list_entry *udev_entry = nullptr;
struct udev_device *device = nullptr;
const char *syspath = nullptr;
string path;
int err;
// Create udev
udev_handle = udev_new();
if (nullptr == udev_handle)
{
return result;
}
// Create enumerator
udev_enum_handle = udev_enumerate_new(udev_handle);
if (nullptr == udev_enum_handle)
{
goto udev_out;
}
// Search only for "hidraw" devices
err = udev_enumerate_add_match_subsystem(udev_enum_handle, "hidraw");
if (err < 0)
{
goto udev_out;
}
// Scan devices
err = udev_enumerate_scan_devices(udev_enum_handle);
if (err < 0)
{
goto udev_out;
}
udev_entry = udev_enumerate_get_list_entry(udev_enum_handle);
if (nullptr == udev_entry)
{
goto udev_out;
}
// Iterate through devices
while (nullptr != udev_entry)
{
// Name is syspath
syspath = udev_list_entry_get_name(udev_entry);
if (nullptr != syspath)
{
device = udev_device_new_from_syspath(udev_handle, syspath);
if (nullptr != device)
{
// devnode is needed /dev/hidraw* path
path = udev_device_get_devnode(device);
// Check vendor id and product id pair
// There's no exit from the loop
// so it will return the last found device
if (check_hidraw_device(path, VENDOR_ID, PRODUCT_ID))
{
result.push_back(path);
}
udev_device_unref(device);
}
}
udev_entry = udev_list_entry_get_next(udev_entry);
}
udev_out:
// Cleanup
if (nullptr != udev_enum_handle)
{
udev_enumerate_unref(udev_enum_handle);
}
udev_unref(udev_handle);
return result;
}
DMM_HID::DMM_HID()
:m_fd(-1)
{
}
DMM_HID::~DMM_HID()
{
Close();
}
void DMM_HID::Open(const std::string& dev)
{
int fd;
if (is_open())
{
Close();
}
fd = open(dev.c_str(), O_RDONLY);
if (fd < 0)
{
return;
}
m_fd = fd;
}
void DMM_HID::Close()
{
if (is_open())
{
close(m_fd);
}
m_fd = -1;
}
bool DMM_HID::is_open()
{
if (m_fd < 0)
{
return false;
}
return true;
}
vector<Reading> DMM_HID::get_readings(int timeout_ms)
{
vector<Reading> result;
uint8_t buf[DMM_BUFSIZE];
Reading reading;
if (m_fd < 0)
{
return result;
}
while(read_exact(m_fd, buf, DMM_BUFSIZE, timeout_ms))
{
reading.value[0] = 0;
reading.value[1] = 0;
reading.value[2] = 0;
reading.value[3] = 0;
reading.modifier = 0;
reading.dot_pos = 0;
reading.is_inf = false;
reading.is_neg = false;
reading.is_DC = false;
reading.is_rel = false;
reading.is_hold = false;
reading.is_min = false;
reading.is_max = false;
reading.type = Reading::reading_type_max;
// Check for infinity
if ((0x4F == buf[1]) && (0x4C == buf[2]))
{
reading.is_inf = true;
}
// BCD 4-bit values
reading.value[0] = (buf[1] >> 4);
reading.value[1] = (buf[1] & 0x0F);
reading.value[2] = (buf[2] >> 4);
reading.value[3] = (buf[2] & 0x0F);
// Byte 0 Bit 6 - Negative
if (buf[0] & 0x40)
{
reading.is_neg = true;
}
// Byte 0 second half - Dot position
{
switch (buf[0] & 0x0F)
{
case 0:
// Do nothing
break;
case 1:
reading.dot_pos = 1;
break;
case 2:
reading.dot_pos = 2;
break;
case 3:
reading.dot_pos = 3;
break;
case 4:
reading.dot_pos = 3;
break;
case 7:
// Assuming 3&4 overlap
reading.dot_pos = 3;
break;
default:
// Do nothing
break;
}
}
// Unit Modifier
{
// Byte 4 Bit 1
if (buf[4] & 0x02)
{
// nano = 10^-9
reading.modifier = -9;
}
// Byte 5 Bit 7
else if (buf[5] & 0x80)
{
// micro = 10^-6
reading.modifier = -6;
}
// Byte 5 Bit 6
else if (buf[5] & 0x40)
{
// mili = 10^-3
reading.modifier = -3;
}
// Byte 5 Bit 5
else if (buf[5] & 0x20)
{
// kilo = 10^3
reading.modifier = 3;
}
// Byte 5 Bit 4
else if (buf[5] & 0x10)
{
// mega = 10^6
reading.modifier = 6;
}
}
// AC/DC
// Byte 3 Bit 4
if (buf[3] & 0x10)
{
reading.is_DC = true;
}
// Hold / min / max / rel
{
// Byte 3 Bit 1
if (buf[3] & 0x02)
{
reading.is_hold = true;
}
// Byte 3 Bit 2
if (buf[3] & 0x04)
{
reading.is_rel = true;
}
// Byte 4 Bit 5
if (buf[4] & 0x20)
{
reading.is_max = true;
}
// Byte 4 Bit 4
if (buf[4] & 0x10)
{
reading.is_min = true;
}
}
// Reading type
{
// if (buf[5] & 0x08)
// {
// reading.type = Reading::reading_continuity;
// }
// else if (buf[5] & 0x04)
// {
// reading.type = Reading::reading_diode;
// }
// else
if (buf[5] & 0x02)
{
reading.type = Reading::reading_percent;
}
else if (buf[6] & 0x80)
{
reading.type = Reading::reading_volts;
}
else if (buf[6] & 0x40)
{
reading.type = Reading::reading_amps;
}
else if (buf[6] & 0x20)
{
reading.type = Reading::reading_ohms;
}
else if (buf[6] & 0x10)
{
reading.type = Reading::reading_hFE;
}
else if (buf[6] & 0x08)
{
reading.type = Reading::reading_hertz;
}
else if (buf[6] & 0x04)
{
reading.type = Reading::reading_farads;
}
else if (buf[6] & 0x02)
{
reading.type = Reading::reading_celsius;
}
else if (buf[6] & 0x01)
{
reading.type = Reading::reading_fahrenheit;
}
}
result.push_back(reading);
}
return result;
}
void DMM_HID::lock()
{
m_mutex.lock();
}
void DMM_HID::unlock()
{
m_mutex.unlock();
}

75
DMM_HID.h Normal file
View File

@ -0,0 +1,75 @@
#ifndef DMM_HID_H_
#define DMM_HID_H_
#include <mutex>
#include <string>
#include <vector>
constexpr int DMM_TIMEOUT_MS = 100;
struct Reading
{
enum reading_type
{
reading_continuity,
reading_diode,
reading_percent,
reading_volts,
reading_amps,
reading_ohms,
reading_hFE,
reading_hertz,
reading_farads,
reading_celsius,
reading_fahrenheit,
reading_type_max
};
uint8_t value[4];
int8_t modifier;
uint8_t dot_pos;
bool is_inf;
bool is_neg;
bool is_DC;
bool is_rel;
bool is_hold;
bool is_min;
bool is_max;
reading_type type;
};
class DMM_HID
{
private:
// File descriptor to DMM device
int m_fd;
std::mutex m_mutex;
public:
DMM_HID();
~DMM_HID();
// Opens the dev file and prepares to get readings
// Returns true on success, false on failure
void Open(const std::string& dev);
// Closes the dev file
void Close();
bool is_open();
// Returns all readings since opening or since last call
std::vector<Reading> get_readings(int timeout_ms = DMM_TIMEOUT_MS);
// Returns all devpaths to DMM devices
static std::vector<std::string> get_dmm_devices();
// Locks the mutex
void lock();
// Unlocks the mutex
void unlock();
};
#endif // DMM_H_

24
Makefile Normal file
View File

@ -0,0 +1,24 @@
src_hid += main_hid.cpp
src_hid += DMM_HID.cpp
src_hid += common.cpp
#src_serial += main_serial.cpp
#src_serial += DMM_Serial.cpp
#src_serial += common.cpp
libs += -lSDL2
libs += -lSDL2_ttf
libs_hid += -ludev
all:
make hid
# make serial
hid:
g++ -o dmm_hid ${src_hid} ${libs} ${libs_hid} -DUSE_HID -Wall -Wextra -Wpedantic
#serial:
# g++ -o dmm_serial ${src_serial} ${libs} -DUSE_Serial -Wall -Wextra -Wpedantic
.PHONY: all hid serial

View File

@ -1,3 +1,22 @@
# dmm_display # DMM Display
Reads measurements from PeakTech 2025 and uses SDL to display it on screen Reads measurements from PeakTech 2025 and uses SDL to display it on screen
The DMM has 2 types of interfaces:
* HID
* Serial
# Requirements
`sudo apt install libsdl2-dev libsdl2-ttf-dev`
# Compiling
`make hid`
or
`make serial`
# Notes
When using HID you need to add hidraw rules in order to use without sudo
`sudo cp 98-hidraw.rules /etc/udev/rules.d/`
`sudo udevadm control --reload-rules`
`sudo udevadm trigger`
# TODO
No serial support yet

207
common.cpp Normal file
View File

@ -0,0 +1,207 @@
#include "common.h"
#include <iostream>
using std::cout;
using std::endl;
bool init_sdl(SDL_Window **window)
{
if (nullptr == window)
{
cout << "Nowhere to store window" << endl;
return false;
}
// SDL Initialization
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
cout << "SDL could not initialize! SDL - " << SDL_GetError() << endl;
return false;
}
(*window) = SDL_CreateWindow("DMM Display",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
1280, 720,
SDL_WINDOW_SHOWN);
if (nullptr == (*window))
{
cout << "Window could not be created! SDL - " << SDL_GetError() << endl;
SDL_Quit();
return false;
}
if (0 != TTF_Init())
{
cout << "Could not init TTF! SDL - "<< SDL_GetError() << endl;
SDL_DestroyWindow(*window);
SDL_Quit();
return false;
}
return true;
}
void quit_sdl(SDL_Window *window)
{
TTF_Quit();
SDL_DestroyWindow(window);
SDL_Quit();
}
bool check_quit()
{
SDL_Event e;
while(SDL_PollEvent(&e))
{
if (SDL_QUIT == e.type)
{
return true;
}
}
return false;
}
void calc_text(std::string& text, std::vector<Reading>& readings)
{
if (readings.empty())
{
return;
}
Reading last = *(readings.rbegin());
text = "";
if (last.is_neg)
{
text += '-';
}
else
{
text += ' ';
}
if (last.is_inf)
{
text += " O L ";
}
else
{
for (int i = 0; i < 4; ++i)
{
text += ('0' + last.value[i]);
text += ' ';
}
text.erase(text.size() - 1);
}
if (last.dot_pos != 0)
{
text[last.dot_pos * 2] = '.';
}
switch (last.modifier)
{
case -9:
text += 'n';
break;
case -6:
text += "μ";
break;
case -3:
text += 'm';
break;
case 0:
text += ' ';
break;
case 3:
text += 'k';
break;
case 6:
text += 'M';
break;
default:
text += ' ';
break;
}
switch (last.type)
{
case Reading::reading_percent:
text += "% ";
break;
case Reading::reading_volts:
text += "V ";
break;
case Reading::reading_amps:
text += "A ";
break;
case Reading::reading_ohms:
text += "Ω ";
break;
case Reading::reading_hFE:
text += "hFE";
break;
case Reading::reading_hertz:
text += "Hz ";
break;
case Reading::reading_farads:
text += "F ";
break;
case Reading::reading_celsius:
text += "°C ";
break;
case Reading::reading_fahrenheit:
text += "°F ";
break;
default:
text += "???";
break;
}
}
void draw_text(const std::string& text, SDL_Surface *surface, TTF_Font *font)
{
SDL_Surface *txt_surface;
SDL_Rect rect;
SDL_Color fg;
SDL_Color bg;
fg.r = 0;
fg.g = 0;
fg.b = 0;
bg.r = 255;
bg.g = 255;
bg.b = 255;
if ((NULL == surface) || (NULL == font) || (0 == text.size()))
{
return;
}
txt_surface = TTF_RenderUTF8_Shaded(font, text.c_str(), fg, bg);
rect.x = 0;
rect.y = surface->h / 2 - txt_surface->h / 2;
SDL_BlitSurface(txt_surface, NULL, surface, &rect);
SDL_FreeSurface(txt_surface);
}

22
common.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef COMMON_H_
#define COMMON_H_
#include <string>
#include <vector>
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#ifdef USE_HID
# include "DMM_HID.h"
#endif
#define UI_FPS 30
bool init_sdl(SDL_Window **window);
void quit_sdl(SDL_Window *window);
bool check_quit();
void calc_text(std::string& text, std::vector<Reading>& readings);
void draw_text(const std::string& text, SDL_Surface *surface, TTF_Font *font);
#endif // COMMON_H_

BIN
font.ttf Normal file

Binary file not shown.

113
main_hid.cpp Normal file
View File

@ -0,0 +1,113 @@
#include <iostream>
#include <memory>
#include <thread>
#include "DMM_HID.h"
#include "common.h"
using std::cout;
using std::endl;
using std::string;
using std::vector;
bool open_dmm(DMM_HID& dmm)
{
vector<string> devs = DMM_HID::get_dmm_devices();
vector<Reading> readings;
if (0 == devs.size())
{
cout << "DMM not found" << endl;
return false;
}
if (devs.size() > 1)
{
cout << "Multiple DMMs found" << endl;
cout << "Using: " << devs[0] << endl;
}
dmm.Open(devs[0]);
// Wait a sec
std::this_thread::sleep_for(std::chrono::seconds(1));
// DMM is open. Now check its config
readings = dmm.get_readings(1);
if (readings.empty())
{
cout << "DMM communication is not ON - Press and hold USB/REL key" << endl;
}
return true;
}
int main(int argc, char **argv)
{
bool ok;
bool quit = false;
DMM_HID dmm;
vector<Reading> readings;
Uint32 next_draw = 0;
Uint32 ms;
SDL_Window *window;
SDL_Surface *surface;
TTF_Font *font;
string text;
// Keep compiler happy
(void)argc;
(void)argv;
ok = open_dmm(dmm);
if (!ok)
{
return -1;
}
ok = init_sdl(&window);
if (!ok)
{
return -1;
}
font = TTF_OpenFont("./font.ttf", 100);
while (!quit)
{
ms = SDL_GetTicks();
if (next_draw - ms > 1000 / UI_FPS)
{
surface = SDL_GetWindowSurface(window);
// Fill the surface white
SDL_FillRect(surface, NULL, SDL_MapRGB(surface->format, 255, 255, 255));
// Get Readings
readings = dmm.get_readings();
// Calculate Text
calc_text(text, readings);
// TODO: Draw Text
draw_text(text, surface, font);
next_draw = ms + 1000 / UI_FPS;
// Update the window
SDL_UpdateWindowSurface(window);
quit = check_quit();
}
}
TTF_CloseFont(font);
quit_sdl(window);
return 0;
}