diff --git a/98-hidraw.rules b/98-hidraw.rules new file mode 100644 index 0000000..57a1ecf --- /dev/null +++ b/98-hidraw.rules @@ -0,0 +1,2 @@ +#Allow all to read hidraws +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev" diff --git a/DMM_HID.cpp b/DMM_HID.cpp new file mode 100644 index 0000000..618dcf1 --- /dev/null +++ b/DMM_HID.cpp @@ -0,0 +1,448 @@ +#include "DMM_HID.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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 DMM_HID::get_dmm_devices() +{ + vector 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 DMM_HID::get_readings(int timeout_ms) +{ + vector 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(); +} diff --git a/DMM_HID.h b/DMM_HID.h new file mode 100644 index 0000000..2833d29 --- /dev/null +++ b/DMM_HID.h @@ -0,0 +1,75 @@ +#ifndef DMM_HID_H_ +#define DMM_HID_H_ + +#include +#include +#include + +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 get_readings(int timeout_ms = DMM_TIMEOUT_MS); + + // Returns all devpaths to DMM devices + static std::vector get_dmm_devices(); + + // Locks the mutex + void lock(); + + // Unlocks the mutex + void unlock(); +}; + +#endif // DMM_H_ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..14913d6 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 78bf264..75cc67d 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +# 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 diff --git a/common.cpp b/common.cpp new file mode 100644 index 0000000..650c48e --- /dev/null +++ b/common.cpp @@ -0,0 +1,207 @@ +#include "common.h" + +#include + +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& 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); +} diff --git a/common.h b/common.h new file mode 100644 index 0000000..6d56e06 --- /dev/null +++ b/common.h @@ -0,0 +1,22 @@ +#ifndef COMMON_H_ +#define COMMON_H_ + +#include +#include + +#include +#include + +#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& readings); +void draw_text(const std::string& text, SDL_Surface *surface, TTF_Font *font); + +#endif // COMMON_H_ diff --git a/font.ttf b/font.ttf new file mode 100644 index 0000000..fdd309d Binary files /dev/null and b/font.ttf differ diff --git a/main_hid.cpp b/main_hid.cpp new file mode 100644 index 0000000..f5167e2 --- /dev/null +++ b/main_hid.cpp @@ -0,0 +1,113 @@ +#include +#include +#include + +#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 devs = DMM_HID::get_dmm_devices(); + vector 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 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; +}