PJON icon indicating copy to clipboard operation
PJON copied to clipboard

bus.reply() does not work when using ThroughSerial

Open codebeat-nl opened this issue 7 years ago • 7 comments

Since yesterday I started to develop a PC Keyboard to Arduino project via ThroughSerial. What I want to achieve is an easy stand-alone prototype tool that can emulate a full-featured keyboard (as buttons) by sending virtual keycodes (byte) and keystate information such as "down", "up", "press" and "released" states (byte) to the Arduino. This can be handy to prototype, do a quick conditional test, navigate thru LCD menus etc, emulate hardware, any button(-state) or to provide a full-featured real-time virtual keyboard to the project in a breeze.

I also want to keep the default debug/monitor facility, or to exchange config information so it must be bi-directional. However there is no bi-directional ThroughSerial example like SoftwareBitBang SendAndReceive, only Transmitter or Receiver examples.

I set up a test project and used the transmiter example in the WINX86 directory and modify it into functions that will be exposed via an Win32 DLL. The reason I do it this way is because want to use it in a Delphi project (and there is no pascal version) to create the actual program, visuals and controls. The test setup is working perfectly fine however in one direction. I read some PJON docs and other bi-directional examples and modified it to a PJON_HALF_DUPLEX communication.

However, when I do a bus reply() after a receive at the Arduino side, it will never trigger the receive handler at the Windows C++ side. After this the bus is locked and will never receive anything. I tried everything, in different order, different options, etc, it will never be triggered (debugged with Visual Studio by set a breakpoint at receiveHandler() function in the C++ file, it will never stop there).

Here is my arduino sketch:

#define TS_BYTE_TIME_OUT 15000
#define TS_RESPONSE_TIME_OUT 60000
/* Maximum accepted timeframe between transmission and synchronous
acknowledgement. This timeframe is affected by latency and CRC computation.
Could be necessary to higher this value if devices are separated by long
physical distance and or if transmitting long packets. */
#define TS_COLLISION_DELAY 3000
#define PJON_INCLUDE_TS true

#include <PJON.h>

#include <SPI.h>
#include <font.h>
#include <LM15SGFNZ07.h>

#define PIN_LCD_LED1    3
#define PIN_LCD_LED2    6
#define PIN_LCD_CS      9
#define PIN_LCD_RST     5
#define PIN_LCD_RS      4

#define COLOR_FG 0xFFF
#define COLOR_BG 0x22F


LM15SGFNZ07 lcd( PIN_LCD_CS , PIN_LCD_RST , PIN_LCD_RS );

// <Strategy name> bus(selected device id)
PJON<ThroughSerial> bus(44);

void receiveHandler(uint8_t *payload, uint16_t length, const PJON_Packet_Info &packet_info) {
  /* Make use of the payload before sending something, the buffer where payload points to is
     overwritten when a new message is dispatched */

  char sLength[3];
  char sData[PJON_PACKET_MAX_LENGTH]; // Needed to 'filter' garbage leftovers after payload's length, 
                                      // it is not null terminated or cleared.
                                 
   // Clear data
  memset( &sData[0], 0, sizeof( sData )); 
   // Copy data
  strncpy( &sData[0], &payload[0], length );
  
  String(length).toCharArray(sLength, 3);
  
  lcd.clear( COLOR_BG );
  lcd.drawString(sData, 1, 1, COLOR_FG, COLOR_BG);
  lcd.drawString(sLength, 1, 20, COLOR_FG, COLOR_BG);
  
  if( payload[0] == 'B' || strcmp((char*)&sData[0], (char*)"Hello World" ) == 0 ) 
  {
    // Can't do this:
    //if(!bus.update()) // If all packets are delivered, send another
    // bus.reply(&sData[0], length);
    
    digitalWrite(LED_BUILTIN, HIGH);
    delay(5);
    digitalWrite(LED_BUILTIN, LOW);
    delay(5);

    //bus.send_packet_blocking(45, "Hello world", 11 );
  }
}

void errorHandler(uint8_t code, uint16_t data, void *custom_pointer) 
{
  char sCode[5] = "     ";
  String(code).toCharArray(sCode, 3);
  
  //lcd.clear( COLOR_BG );
  lcd.drawString(sCode, 1, 50, COLOR_FG, COLOR_BG);
};



void setup() {
  
   // Serial setup
  Serial.begin(115200); //9600);
  
   // init the lcd
  lcd.init();
  delay(1000);
  lcd.clear( COLOR_BG );

  
   // Set backlight
  pinMode( PIN_LCD_LED1  , OUTPUT );
  pinMode( PIN_LCD_LED2  , OUTPUT );

   
   // Maximum brightness
  analogWrite( PIN_LCD_LED1, 0xFF );
  analogWrite( PIN_LCD_LED2, 0xFF );

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW); // Initialize LED 13 to be off

  bus.strategy.set_serial(&Serial);
  bus.set_communication_mode( PJON_HALF_DUPLEX );
  //bus.include_sender_info(false);
  //bus.set_synchronous_acknowledge(false);
  bus.set_receiver( receiveHandler );
  
  bus.begin();
  //bus.send_packet_blocking(45, "Hello world", 11 );
};

void loop() 
{
  bus.receive();
  bus.update();
}

Here is my Visual Studio C++ DLL code:

#include "pjonser32.h"  


static void dataReceiveHandler( uint8_t *payload, uint16_t length )
{
	if (pReceiveCallbackProc != NULL)
	{
		try {
			pReceiveCallbackProc(payload, length);
		}
		catch (const char* msg)
		{
			errorCode = PJON_ERR_CALLBACK_EXCEPTION;
			bRecvPollenabled = false;
			pReceiveCallbackProc = NULL;
		}
	}
}


static void receiveHandler(uint8_t *payload, uint16_t length, const PJON_Packet_Info &packet_info)
{
	// I set the breakpoint here, never called:
       dataReceiveHandler( payload, length);
};


bool  __stdcall recvhndlr(PJONReceiveCallbackProc pReceiveProc)
{
	errorCode = PJON_ERR_NO_ERROR;
	
	if (pReceiveProc != NULL)
	{
		char sTest[] = "TEST\0";

		pReceiveCallbackProc = pReceiveProc;
		dataReceiveHandler( (uint8_t*)&sTest[0], 4 );
	    
		if( errorCode != PJON_ERR_NO_ERROR )
		 {
			pReceiveCallbackProc = NULL;
		 }
	}
	else { pReceiveCallbackProc = NULL; }
	
	bRecvPollenabled = (errorCode == PJON_ERR_NO_ERROR && pSerialBus != NULL);

	return (errorCode == PJON_ERR_NO_ERROR);
}

bool  __stdcall recvupd()
{
	if (bRecvPollenabled)
	{
		try {
			pSerialBus->receive();
			pSerialBus->update();
		}
		catch (const char* msg)
		{
			return false;
		}
	    
		return true;
	}
	
	return false;
}


static void errorHandler(uint8_t code, uint16_t data, void *custom_pointer) 
{
	errorCode = code;
};



bool __stdcall stop()
{
	    bRecvPollenabled = false;
		
		if (pSerialBus != NULL)
		{
			delete pSerialBus;
			pSerialBus = NULL;

			if (pSerialHandle != NULL)
			{
				delete pSerialHandle;
				pSerialHandle = NULL;
			}

			return true;
		}

		return false;
}

byte __stdcall lasterr()
{
	byte err = errorCode;
	errorCode = 0;
	return err;
}

bool __stdcall start(byte iComPort, DWORD dBitRate, byte iDeviceId, byte iTargetDeviceId)
{
		if (pSerialBus != NULL)
		{
			errorCode = PJON_ERR_ALREADY_INIT;
			return false; 
		}
		
		bRecvPollenabled = false;

		bool bResult = ( iComPort > 0 && (dBitRate >= 75 && dBitRate <= 115200) && iDeviceId > 0 && iTargetDeviceId > 0 && iDeviceId != iTargetDeviceId);
		char sCom[] = "COM\0\0\0";


		if (bResult)
		{
			_itoa_s(iComPort, &sCom[3], 3, 10);

			// PJON instantiation...
			try {
				pSerialBus = new PJONThroughSerial( iDeviceId );
			}
			catch (const char* msg)
			{
				errorCode = PJON_ERR_CANNOT_INIT;
				bResult   = false;
			}
		}
		else { errorCode = PJON_ERR_WRONG_PARAMS; }

		if (bResult)
		{

			// Set here the COM port assigned to the device you want to communicate with
			tstring commPortName = tstring(TEXT(&sCom[0]));

			try {
				// Opening serial...
				pSerialHandle = new Serial(commPortName, dBitRate, false, false);

				// Setting serial... 
				pSerialBus->strategy.set_serial(pSerialHandle);
				pSerialBus->set_communication_mode( PJON_HALF_DUPLEX );
				//pSerialBus->include_sender_info(false);
				//pSerialBus->set_synchronous_acknowledge(false);
	
				// Opening bus... 
				pSerialBus->begin();
				pSerialBus->set_receiver(receiveHandler);
				pSerialBus->set_error(errorHandler);
			}
			catch (const char* msg)
			{
				errorCode = PJON_ERR_CANNOT_OPEN_COM;
				bResult = false;
			}
		}

		if (bResult)
		{
			errorCode         = PJON_ERR_NO_ERROR;
			deviceSourceBusId = iDeviceId;
			deviceTargetBusId = iTargetDeviceId;
			bRecvPollenabled = (pReceiveCallbackProc != NULL);
		}
		else { stop(); }

		return bResult;
}



bool  __stdcall send(byte* uByteData, DWORD iLength)
{
	errorCode = (pSerialBus == NULL)?PJON_ERR_NOT_INIT:(iLength > 0?PJON_ERR_NO_ERROR: PJON_ERR_NO_CONTENT);

	if( errorCode == PJON_OK )
	{
		bool b = bRecvPollenabled;
		bRecvPollenabled = false;

		if (pSerialBus->send_packet(deviceTargetBusId, (char*)uByteData, iLength) == PJON_ACK)
		{
				pSerialBus->receive(1000); // 1000000);
				pSerialBus->update();
				bRecvPollenabled = b;
				
				return ( errorCode == PJON_OK );
		}
		else {
				 // Looks silly however errorHandler() can be triggered and maybe global var has been changed
				if (errorCode == PJON_OK)
				{
					errorCode = PJON_ERR_CANNOT_SEND;
				}
			}
		
		bRecvPollenabled = b;
	}

	return false;
}

And the h file:

extern "C"


#include <stdio.h>
#include <iostream>
// PJON library
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <chrono>


#ifndef WINX86
#define WINX86 true
#endif

#define TS_BYTE_TIME_OUT 15000

#define TS_RESPONSE_TIME_OUT 60000
/* Maximum accepted timeframe between transmission and synchronous
acknowledgement. This timeframe is affected by latency and CRC computation.
Could be necessary to higher this value if devices are separated by long
physical distance and or if transmitting long packets. */

#define TS_COLLISION_DELAY 3000

 // Include only Serial
#define PJON_INCLUDE_TS true

#include <PJON.h>  

#include <WinSDKVer.h>

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>

// types
#define PSerial Serial*
#define PJONThroughSerial   PJON<ThroughSerial>
#define PPJONThroughSerial  PJON<ThroughSerial>*
typedef void (__stdcall *PJONReceiveCallbackProc)( uint8_t* uData, uint16_t iLength );



// 'consts'
#define PJON_OK						0
#define PJON_ERR_NO_ERROR			0
#define PJON_ERR_CANNOT_INIT			1
#define PJON_ERR_CANNOT_OPEN_COM		2
#define PJON_ERR_CANNOT_SEND			3
#define PJON_ERR_NOT_INIT			4
#define PJON_ERR_NO_CONTENT			5
#define PJON_ERR_ALREADY_INIT		6
#define PJON_ERR_WRONG_PARAMS		7
#define PJON_ERR_CALLBACK_EXCEPTION 8
#define PJON_ERR_CONNECTION_LOST     PJON_CONNECTION_LOST      
#define PJON_ERR_PACKETS_BUFFER_FULL PJON_PACKETS_BUFFER_FULL
#define PJON_ERR_CONTENT_TOO_LONG    PJON_CONTENT_TOO_LONG     
#define PJON_ERR_ID_ACQUISITION_FAIL PJON_ID_ACQUISITION_FAIL
#define PJON_ERR_DEVICES_BUFFER_FULL PJON_DEVICES_BUFFER_FULL 

// vars
byte						deviceSourceBusId    = 45;
byte						deviceTargetBusId    = 44;
byte						errorCode            = 0;
PSerial					pSerialHandle        = NULL;
PPJONThroughSerial		pSerialBus           = NULL;
PJONReceiveCallbackProc pReceiveCallbackProc = NULL;
bool                    bRecvPollenabled     = false;


The def file:

LIBRARY pjonser32
EXPORTS
 start
 recvhndlr
 recvupd
 send
 lasterr
 stop

And the Delphi Form file:

unit main;

interface

uses
  Windows, Messages, SysUtils,  Classes, Graphics,
  Controls, Forms, Dialogs,  StdCtrls, ExtCtrls;

type
 TPJONReceiveCallbackProc = procedure( uData : PChar; iLength : DWORD ); stdcall;
 PPJONReceiveCallbackProc = ^TPJONReceiveCallbackProc;

  TForm1 = class(TForm)
    Button1: TButton;
    Label1: TLabel;
    Edit1: TEdit;
    Label2: TLabel;
    Edit2: TEdit;
    Label3: TLabel;
    cbSerialPower: TCheckBox;
    Timer1: TTimer;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure cbSerialPowerClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

const
 PJONSER_DLLNAME = 'pjonser32.dll';


// Error codes
const
 PJON_OK                      = 0;
 PJON_ERR_NO_ERROR            = 0;
 PJON_ERR_CANNOT_INIT         = 1;
 PJON_ERR_CANNOT_OPEN_COM     = 2;
 PJON_ERR_CANNOT_SEND         = 3;
 PJON_ERR_NOT_INIT            = 4;
 PJON_ERR_NO_CONTENT          = 5;
 PJON_ERR_ALREADY_INIT        = 6;
 PJON_ERR_WRONG_PARAMS        = 7;
 PJON_ERR_CALLBACK_EXCEPTION  = 8;


 PJON_ERR_CONNECTION_LOST     = 101;
 PJON_ERR_PACKETS_BUFFER_FULL = 102;
 PJON_ERR_CONTENT_TOO_LONG    = 104;
 PJON_ERR_ID_ACQUISITION_FAIL = 105;
 PJON_ERR_DEVICES_BUFFER_FULL = 254;

var
  Form1: TForm1;


function pjonSerialStart( iComPort : byte; dBitRate : DWORD; iDeviceId : byte; iTargetDeviceId : byte ) : bool; stdcall; external PJONSER_DLLNAME name 'start';
function pjonSerialSetReceiveHandlerProc( pReceiveProc : PPJONReceiveCallbackProc ) : bool; stdcall; external PJONSER_DLLNAME name 'recvhndlr';
function pjonSerialReceiveUpdate() : bool; stdcall; external PJONSER_DLLNAME name 'recvupd';
function pjonSerialSend( uByteData : PChar; iLength : DWORD ) : bool; stdcall; external PJONSER_DLLNAME name 'send';
function pjonSerialLastErr() : byte; stdcall; external PJONSER_DLLNAME name 'lasterr';
function pjonSerialStop() : bool; stdcall; external PJONSER_DLLNAME name 'stop';


implementation

{$R *.dfm}

procedure ReceiveCallbackProc( uData : PChar; iLength : DWORD ); stdcall;
begin
 Form1.Edit2.Text:= string( uData );
 Form1.Label3.Caption:=IntToStr( iLength )+' - '+IntToStr( pjonSerialLastErr() );
end;



procedure TForm1.Button1Click(Sender: TObject);
begin
 pjonSerialSend( PChar( Edit1.Text+#0 ), length( Edit1.Text ) );
 Label2.Caption:=IntToStr( pjonSerialLastErr() );
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 pjonSerialSetReceiveHandlerProc( @ReceiveCallbackProc );
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  pjonSerialStop();
end;

procedure TForm1.cbSerialPowerClick(Sender: TObject);
begin
 if( cbSerialPower.Checked ) then
  begin
   cbSerialPower.Checked:= pjonSerialStart( 1, 115200, 45, 44 );
   if( cbSerialPower.Checked ) then
    begin
      pjonSerialSend( PChar( Edit1.Text+#0 ), length( Edit1.Text ) );
    end;

    Label2.Caption:=IntToStr( pjonSerialLastErr() );
  end;

 if( NOT cbSerialPower.Checked ) then
  pjonSerialStop();
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
 pjonSerialReceiveUpdate();
end;

end.




afbeelding

Can it be solved? Many thanks for any reply.

codebeat-nl avatar Jun 16 '18 19:06 codebeat-nl

Ciao @codebeat-nl your project seems really interesting! You may are experiencing a bug, if using an Arduino compatible device with a hardware serial port, the hang may be triggered by a collision (both devices transmits at the same time) although I am still digging to understand.

If you want to go further without waiting for the fix:

  1. Call bus.set_synchronous_acknowledge(false) on both sides
  2. Structure the exchange to avoid any possible collision or the chance that 2 packets are sent at the same time, that means to use a master slave approach where the master provokes a slave response: master request -> (small delay) -> slave response

Following point 1 and 2 should be a safe workaround and also a stable procedure. I am sorry, I hope to get a fix for this as soon as possible.

Feel free to request for support if required :)

gioblu avatar Jun 17 '18 00:06 gioblu

@codebeat-nl it seems a fairly easy way to get a clean and professional custom solution, an example could be a cool entry point for other users like me that have never tried Delphi before. Will study that more in detail.

gioblu avatar Jun 17 '18 02:06 gioblu

Ciao @codebeat-nl did the solutions 1 or 2 solved your issue? Thank you again :)

gioblu avatar Jun 18 '18 06:06 gioblu

Ciao @gioblu , thanks for the replies. Nearly close to the weekend I just want to let you know I am working on it, the solutions you provide doesn't help because I tried that before. With bus.set_synchronous_acknowledge(false) it lost it's security to be reliable (unreliable like a normal serial connection).

However, make some progress at the keyboard 'interface' design, it is working great and missing bidirectional doesn't have to be a problem (when doing it the 'smart' way) to be a keyboard or mouse emulator.

To be a simple solution to everybody, out-of-the-box solution, bidirectional must be possible because it also provides debug possibilities. So this is still a wanted/required feature.

Just want to let you know, please don't close the question/bug. This weekend I will try to show my findings.

codebeat-nl avatar Jun 22 '18 01:06 codebeat-nl

Ciao @codebeat-nl thank you very much for your answer, For sure, feel free to contact me directly on gitter if you need my support.

gioblu avatar Jun 22 '18 02:06 gioblu

[edited] Sorry, false positive. :) My issue seems to be not not related to this.

YuriySm avatar Aug 22 '18 20:08 YuriySm

Ciao @codebeat-nl I think I may have found the problem thanks to the report of @Girgitt, Could you give it a try and see if the latest referenced commit solves your issue on Windows? Thank you again :)

gioblu avatar Aug 30 '18 14:08 gioblu