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
The DMM has 2 types of interfaces:
* HID
* Serial
Reads measurements from PeakTech 2025 and uses SDL to display it on screen
# 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;
}