Tasmota/tasmota/tasmota_xsns_sensor/xsns_107_gm861.ino

410 lines
16 KiB
C++

/*
xsns_107_gm861.ino - Support for GM861 Bar Code Reader for Tasmota
Copyright (C) 2023 Theo Arends
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_GM861
/*********************************************************************************************\
* GM861 1D and 2D Bar Code Reader
*
* For background information see https://github.com/arendst/Tasmota/discussions/18399
*
* Commands available:
* GM861Zone<byte> - Show zone byte state
* GM861Zone<byte> <value> - Set zone byte to value
* GM861Zone5 20 - Set read interval to 2 seconds (default 10 = 0x0A)
* GM861Zone44 0x02 - Enable all barcodes using full area (default 5 = 0x05)
* GM861Zone208 0x80 - Enable AIM ID (default 0 = 0x00)
* GM861Save - Save changes in zone bytes to flash
* GM861Reset 1 - Reset to factory settings and re-init the scanner
* GM861Dump - Dump zone bytes 0 to 97 if logging level 4
\*********************************************************************************************/
#define XSNS_107 107
//#define GM861_DECODE_AIM // Decode AIM-id (+0k3 code)
//#define GM861_HEARTBEAT // Enable heartbeat
#define GM861_BAUDRATE 9600 // Serial baudrate
#define GM861_GUI_LENGTH 30 // Max number of code characters in GUI
/*
#define GPIO_GM861_TX 304
#define D_SENSOR_GM861_TX "GM861 Tx"
#define GPIO_GM861_RX 305
#define D_SENSOR_GM861_RX "GM861 Rx"
Command ===================== Response ============== Description =========================================================
Headr Ty Ln Addrs Data Check Headr Ty Ln Data Check
----- -- -- ----- ----- ----- ----- -- -- ----- -----
7E 00 07 01 00 2A 02 D8 0F 02 00 00 02 39 01 C1 4C Get baudrate (9600)
7E 00 08 02 00 2A 39 01 A7 EA 02 00 00 02 39 01 SS SS Set baudrate to 9600
7E 00 08 01 00 00 D6 AB CD LED on, Mute off, Normal lighting, Normal brightness, Continuous mode
7E 00 08 01 00 02 01 AB CD 02 00 00 01 00 33 31 Command trigger Mode (Set bit0 of zone byte 0x0002)
7E 00 08 01 00 02 01 AB CD Trigger mode
7E 00 08 01 00 2C 02 AB CD Full area, Allow read all bar codes
7E 00 08 01 00 36 01 AB CD Allow read Code39
7E 00 08 01 00 37 00 AB CD Code39 Min length
7E 00 08 01 00 38 FF AB CD Code39 Max length
7E 00 08 01 00 B0 01 AB CD Cut out data:Output Start part
7E 00 08 01 00 B1 FF AB CD Cut out M bytes from start
7E 00 08 01 00 B0 02 AB CD Output End part
7E 00 08 01 00 B2 FF AB CD Cut out N bytes from end
7E 00 08 01 00 B0 03 AB CD Output center part
7E 00 08 01 00 B1 03 AB CD Cut out N bytes from start (Eg: three characters)
7E 00 08 01 00 B2 02 AB CD Cut out N bytes from end (Eg: two characters)
7E 00 08 01 00 D0 80 AB CD Enabling AIM ID
7E 00 08 01 00 D0 00 AB CD Disabling AIM ID
7E 00 08 01 00 D9 50 81 D3 02 00 00 01 00 33 31 Zone bytes reset to defaults
7E 00 08 01 00 D9 55 D1 76 02 00 00 01 00 33 31 Restore user-defined factory settings
7E 00 08 01 00 D9 56 E1 15 02 00 00 01 00 33 31 Save as user-defined factory settings
7E 00 08 01 00 D9 A5 3E 69 02 00 00 01 00 33 31 Deep sleep, can be awakened by serial port interrupt, this serial port command is invalid
7E 00 08 01 00 D9 00 DB 26 02 00 00 01 00 33 31 Wake up from sleep
7E 00 08 01 00 E7 00 AB CD OUT1 Output low level
7E 00 08 01 00 E7 01 AB CD OUT1 Output high level
7E 00 09 01 00 00 00 DE C8 02 00 00 01 00 33 31 Save settings to flash
7E 00 0A 01 00 00 00 30 1A 03 00 00 01 00 33 31 Send heartbeat every 10 seconds
*/
enum Gm861States {
GM861_STATE_DONE,
GM861_STATE_DUMP,
GM861_STATE_SERIAL_OUTPUT,
GM861_STATE_OUTPUT,
GM861_STATE_SETUP_CODE_ON,
GM861_STATE_INIT_OFFSET = 16, // Init after (GM861_STATE_INIT_OFFSET - GM861_STATE_SETUP_CODE_ON) * 0.25 seconds restart
GM861_STATE_RESET
};
#include <TasmotaSerial.h>
TasmotaSerial *Gm861Serial = nullptr;
typedef struct {
char barcode[GM861_GUI_LENGTH];
uint8_t index;
uint8_t state;
uint8_t heartbeat;
bool read;
} tGm861;
tGm861 *Gm861 = nullptr;
/*********************************************************************************************/
#ifdef GM861_DECODE_AIM
const char kGm861AIMID[] PROGMEM = "A1C0E0E4F0G0H1I0I1L0M1Q1R0S0X0X1X4X5d1emzm";
const char kGm861AIM[] PROGMEM = "Code39|Code128|EAN13|EAN8|Codabar|Code93|Code11|I2of5|ITF|PDF417|MSIPlessey|QRCode|S2of5|D2of5|CnPost|M2of5|ISBN|ISSN|DMCode|GS1|Aztec";
String Gm861AIMId2AIM(const char* aim_id) {
char aim_ids[sizeof(kGm861AIMID)];
strcpy_P(aim_ids, kGm861AIMID);
int index = (strstr(aim_ids, aim_id) -aim_ids) /2;
if (index < 0) { // Unknown
strcpy(aim_ids, aim_id); // Return AIM-id
} else {
GetTextIndexed(aim_ids, sizeof(aim_ids), index, kGm861AIM);
}
return aim_ids;
}
#endif // GM861_DECODE_AIM
uint32_t Gm861Crc(uint8_t* ptr, uint32_t len) {
// When no need for checking CRC, CRC byte can be filled in 0xAB 0xCD
uint32_t crc = 0;
while (len-- != 0) {
for (uint8_t i = 0x80; i != 0; i /= 2) {
crc *= 2;
if ((crc & 0x10000) !=0) { // After multiplying the last bit of CRC by 2, if the first bit is 1, then divide by 0x11021
crc ^= 0x11021;
}
if ((*ptr & i) != 0) { // If this bit is 1, then CRC = previous CRC + this bit/CRC_CCITT
crc ^= 0x1021;
}
}
ptr++;
}
return crc;
}
void Gm861Send(uint32_t type, uint32_t len, uint32_t address, uint32_t data) {
uint8_t buffer[10];
buffer[0] = 0x7E;
buffer[1] = 0;
buffer[2] = type;
buffer[3] = len;
buffer[4] = 0;
buffer[5] = address;
buffer[6] = data;
uint8_t index = 7;
if (len > 1) {
buffer[7] = data >> 8;
index++;
}
uint32_t crc = Gm861Crc(buffer+2, len + 4);
buffer[index] = crc >> 8;
index++;
buffer[index] = crc;
index++;
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, (uint8_t*)buffer, index);
Gm861Serial->write(buffer, index);
}
void Gm861SetZone(uint32_t address, uint32_t data) {
Gm861->read = false;
uint32_t len = 1;
if (0x2A == address) { len = 2; } // Baudrate
Gm861Send(8, len, address, data);
}
void Gm861GetZone(uint32_t address) {
Gm861->read = true;
Gm861->index = address;
uint32_t data = 1;
if (0x2A == address) { data = 2; } // Baudrate
Gm861Send(7, 1, address, data);
}
void Gm861SerialInput(void) {
if (!Gm861Serial->available()) { return; }
char buffer[272]; // Max 256 code characters and some control characters
uint32_t byte_counter = 0;
// Use timeout to allow large serial reads within a window
uint32_t timeout = millis();
while (millis() - 10 < timeout) {
if (Gm861Serial->available()) {
timeout = millis();
buffer[byte_counter++] = Gm861Serial->read();
if (byte_counter >= sizeof(buffer) -1) { break; }
}
}
buffer[byte_counter] = 0; // Add string terminating zero
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, (uint8_t*)buffer, byte_counter);
if (0 == buffer[1]) { // Command result or Heartbeat
if (2 == buffer[0]) { // Command result
// 02 00 00 01 00 33 31 - Command acknowledge
// 02 00 00 01 01 23 10 - Command result (Zonebyte 96 - 0x60)
// 02 00 00 02 39 01 C1 4C - Command result (Zonebytes 42/43 - 0x2A)
if (Gm861->read) {
uint32_t result = buffer[4];
if (2 == buffer[3]) { // Length
result += (buffer[5] << 8);
}
Gm861->read = false;
Response_P(S_JSON_COMMAND_INDEX_NVALUE, PSTR("GM861Zone"), Gm861->index, result);
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR("GM861Zone"));
}
}
else if (3 == buffer[0]) { // Heartbeat
// 03 00 00 01 00 33 31 - Heartbeat response
// 03 00 0A 30 33 32 31 35 36 33 38 34 30 - Scan response with zone byte 96 = 0x81
#ifdef GM861_HEARTBEAT
if ((0 == buffer[2]) && (1 == buffer[3]) && (0 == buffer[4])) {
Gm861->heartbeat = 0;
}
#endif // GM861_HEARTBEAT
}
} else { // Bar code
// 38 37 31 31 32 31 38 39 37 32 38 37 35 0D - Barcode 8711218972875
// 5D 45 30 38 37 31 31 32 31 38 39 37 32 38 37 35 0D - AIM ]E0, Barcode 8711218972875
RemoveControlCharacter(buffer); // Remove control character (0x00 .. 0x1F and 0x7F)
uint32_t offset = 0;
char aim_id[3] = { 0 };
if (']' == buffer[0]) { // AIM code ]xy
offset = 3;
aim_id[0] = buffer[1];
aim_id[1] = buffer[2];
}
// Prepare GUI result
snprintf_P(Gm861->barcode, sizeof(Gm861->barcode) -3, PSTR("%s"), buffer + offset);
if (strlen(buffer) > sizeof(Gm861->barcode) -3) {
strcat(Gm861->barcode, "...");
}
ResponseTime_P(PSTR(",\"GM861\":{"));
if (offset) {
#ifdef GM861_DECODE_AIM
ResponseAppend_P(PSTR("\"AIM\":\"%s\","), Gm861AIMId2AIM(aim_id).c_str());
#else
ResponseAppend_P(PSTR("\"AIM\":\"%s\","), aim_id);
#endif // GM861_DECODE_AIM
}
ResponseAppend_P(PSTR("\"Code\":\"%s\"}}"), buffer + offset);
MqttPublishTeleSensor();
}
}
/********************************************************************************************/
void Gm861Init(void) {
if (PinUsed(GPIO_GM861_RX) && PinUsed(GPIO_GM861_TX)) {
Gm861 = (tGm861*)calloc(sizeof(tGm861), 1);
if (!Gm861) { return; }
Gm861Serial = new TasmotaSerial(Pin(GPIO_GM861_RX), Pin(GPIO_GM861_TX), 1);
if (Gm861Serial->begin(GM861_BAUDRATE)) {
if (Gm861Serial->hardwareSerial()) {
ClaimSerial();
}
#ifdef ESP32
AddLog(LOG_LEVEL_DEBUG, PSTR("GM8: Serial UART%d"), Gm861Serial->getUart());
#endif
Gm861->barcode[0] = '0'; // No barcode yet
Gm861->state = GM861_STATE_INIT_OFFSET;
}
}
}
void Gm861StateMachine(void) {
/*
Power on
14:25:04.219-025 DMP: 02 00 00 62 D6 00 20 00 00 0A 32 01 2C 00 87 3C 01 A1 1C 32 03 00 80 00 06 00 00 00 00 00 00 00 00 00 00 00 00 02 80 3C 00 00 00 06 00 00 39 01 05 64 0D 0D 0D 01 0D 01 04 80 09 04 80 05 04 80 01 04 80 01 08 04 80 08 04 80 08 04 80 08 04 80 08 01 80 00 00 00 04 80 01 01 00 00 00 00 04 80 00 00 03 00 01 00 30 FE
Default: setup code on
12:12:18.672-024 DMP: 02 00 00 62 D6 00 20 00 00 0A 32 01 2C 00 87 3C 01 A1 1C 32 03 00 80 00 06 00 00 00 00 00 00 00 00 00 00 00 00 02 80 3C 00 00 00 06 00 00 39 01 05 64 0D 0D 0D 01 0D 01 04 80 09 04 80 05 04 80 01 04 80 01 08 04 80 08 04 80 08 04 80 08 04 80 08 01 80 00 00 00 04 80 01 01 00 00 00 00 04 80 00 00 03 00 01 00 30 FE
Output:
14:37:45.129-027 DMP: 02 00 00 62 D6 00 20 01 00 0A 32 01 2C 00 87 3C 01 A1 1C 32 03 00 80 00 06 00 00 00 00 00 00 00 00 00 00 00 00 02 80 3C 00 00 00 06 00 00 39 01 05 64 0D 0D 0D 01 0D 01 04 80 09 04 80 05 04 80 01 04 80 01 08 04 80 08 04 80 08 04 80 08 04 80 08 01 80 00 00 00 04 80 01 01 00 00 00 00 04 80 00 00 03 00 01 00 20 91
Serial output:
14:39:04.887-027 DMP: 02 00 00 62 D6 00 20 01 00 0A 32 01 2C 00 87 3C 01 A0 1C 32 03 00 80 00 06 00 00 00 00 00 00 00 00 00 00 00 00 02 80 3C 00 00 00 06 00 00 39 01 05 64 0D 0D 0D 01 0D 01 04 80 09 04 80 05 04 80 01 04 80 01 08 04 80 08 04 80 08 04 80 08 04 80 08 01 80 00 00 00 04 80 01 01 00 00 00 00 04 80 00 00 03 00 01 00 2D 9E
*/
if (!Gm861->state) { return; }
switch (Gm861->state) {
case GM861_STATE_RESET:
Gm861SetZone(0xD9, 0x50); // Factory reset
Gm861->state = GM861_STATE_SETUP_CODE_ON +7; // Add time for reset to complete
break;
case GM861_STATE_SETUP_CODE_ON:
Gm861SetZone(0x00, 0xD6); // Set LED on, Mute off, Normal lighting, Normal brightness, Continuous mode (Default: setup code on)
break;
case GM861_STATE_OUTPUT:
Gm861SetZone(0x03, 0x01); // Enable output (Output)
break;
case GM861_STATE_SERIAL_OUTPUT:
Gm861SetZone(0x0D, 0xA0); // Enable serial port output (Serial Output)
break;
case GM861_STATE_DUMP:
Gm861Send(7, 1, 0, 0x62); // Dump zone bytes 0 to 97
AddLog(LOG_LEVEL_INFO, PSTR("GM8: Initialized"));
break;
}
Gm861->state--;
}
#ifdef GM861_HEARTBEAT
void Gm861Heartbeat(void) {
if (!Gm861->state && (!(TasmotaGlobal.uptime % 10))) {
// It is recommended to send a heartbeat packet every 10 seconds
Gm861Send(10, 1, 0, 0); // Send heartbeat
Gm861->heartbeat++;
// If no correct reply is received for three consecutive times, the main control should be handle it accordingly.
if (Gm861->heartbeat > 3) {
AddLog(LOG_LEVEL_DEBUG, PSTR("GM8: Heartbeat lost"));
Gm861->heartbeat = 0;
Gm861->state = GM861_STATE_RESET;
}
}
}
#endif // GM861_HEARTBEAT
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
const char kGm861Commands[] PROGMEM = "GM861|" // Prefix
"Zone|Save|Reset|Dump";
void (* const Gm861Command[])(void) PROGMEM = {
&CmndGm816Zone, &CmndGm816Save, &CmndGm816Reset, &CmndGm816Dump };
void CmndGm816Zone(void) {
// GM861Zone42 - Read baudrate
// GM861Zone0 0xD6 - Set LED on, Mute off, Normal lighting, Normal brightness, Continuous mode
if ((XdrvMailbox.index >= 0x00) && (XdrvMailbox.index <= 0xD9)) {
if ((XdrvMailbox.payload >= 0x00) && (XdrvMailbox.payload <= 0x09C4)) {
Gm861SetZone(XdrvMailbox.index, XdrvMailbox.payload);
ResponseCmndIdxNumber(XdrvMailbox.payload);
} else {
Gm861GetZone(XdrvMailbox.index);
ResponseClear();
}
}
}
void CmndGm816Save(void) {
// GM861Save - Save zone bytes to flash
Gm861Send(9, 1, 0, 0); // Save to flash
ResponseCmndDone();
}
void CmndGm816Reset(void) {
// GM861Reset 1 - Do factory reset and inititalize for serial comms
if (1 == XdrvMailbox.payload) {
Gm861->state = GM861_STATE_RESET;
ResponseCmndDone();
}
}
void CmndGm816Dump(void) {
// GM861Dump - Dump zone bytes 0 to 97. Needs logging level 4
Gm861Send(7, 1, 0, 0x62); // Dump zone bytes 0 to 97
ResponseCmndDone();
}
/*********************************************************************************************\
Interface
\*********************************************************************************************/
bool Xsns107(uint32_t function) {
bool result = false;
if (FUNC_INIT == function) {
Gm861Init();
}
else if (Gm861Serial) {
switch (function) {
case FUNC_LOOP:
case FUNC_SLEEP_LOOP:
Gm861SerialInput();
break;
case FUNC_EVERY_250_MSECOND:
Gm861StateMachine();
break;
#ifdef GM861_HEARTBEAT
case FUNC_EVERY_SECOND:
Gm861Heartbeat();
break;
#endif // GM861_HEARTBEAT
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
WSContentSend_PD(PSTR("{s}GM816{m}%s{e}"), Gm861->barcode);
break;
#endif // USE_WEBSERVER
case FUNC_COMMAND:
result = DecodeCommand(kGm861Commands, Gm861Command);
break;
}
}
return result;
}
#endif // USE_GM861