How To Develop New Adapter
Lin
adapter is similar to Can
adapter, so you can refer to the Can
adapter for development.
Prerequisites
- node-gyp - For building native modules
- napi - Node.js Native API
- swig - Simplified Wrapper and Interface Generator
Create Adapter Folder
Create your adapter in the following directory:
src/main/docan/${adapter_name}
Directory Structure
Each adapter directory should follow this structure:
${adapter_name}/
├── index.ts # Main adapter implementation file
├── swig/ # SWIG interface definitions
│ ├── ${adapter_name}.i # Main SWIG interface file
│ ├── buffer.i # Buffer handling interface
│ └── s.bat # Build script
├── inc/ # C/C++ header files
└── lib/ # Third-party library files
SWIG Interface Implementation
The SWIG interface is crucial for bridging between JavaScript/TypeScript and native C/C++ code. Here's how to implement it:
1. Main Interface File (${adapter_name}.i)
Create a main interface file in the swig
directory:
%module ${adapter_name}
%header %{
#include <windows.h>
#include <stdlib.h>
#include "your_native_header.h"
%}
%include <stdint.i>
%include <windows.i>
%include "your_native_header.h"
// Define typemaps for buffer handling
%include "./buffer.i"
%typemap(in) (void *data, size_t length) = (const void* buffer_data, const size_t buffer_len);
// Define pointer and array classes
%include <cpointer.i>
%pointer_class(unsigned long, JSUINT64)
%pointer_class(long, JSINT64)
%array_class(uint8_t, ByteArray);
%init %{
// Add initialization code here
%}
2. Buffer Interface (buffer.i)
Create a buffer interface file for handling binary data:
%typemap(in) (const size_t buffer_len,const void* buffer_data) {
if ($input.IsBuffer()) {
Napi::Buffer<char> buf = $input.As<Napi::Buffer<char>>();
$2 = reinterpret_cast<void *>(buf.Data());
$1 = buf.ByteLength();
} else {
SWIG_exception_fail(SWIG_TypeError, "in method '$symname', argument is not a Buffer");
}
}
3. Build Script (s.bat)
Create a build script to generate the SWIG wrapper:
swig -I"./../inc" -c++ -javascript -napi -v ./${adapter_name}.i
Key Points for SWIG Implementation:
Type Mapping
- Use
%typemap
for C/C++ to JavaScript type conversion - Handle buffers and pointers properly
- Consider endianness for binary data
- Use
Header Inclusion
- Include system and library headers
- Use
%header %{ ... %}
for C/C++ code - Use
%include
for SWIG interfaces
Error Handling
- Implement error checking in typemaps
- Use
SWIG_exception_fail
for errors - Handle memory management carefully
Module Initialization
- Use
%init %{ ... %}
for setup code - Register callbacks if needed
- Initialize global variables
- Use
Common SWIG Directives:
%module name // Define module name
%include file // Include SWIG interface
%header %{ ... %} // Include C/C++ code
%typemap(...) ... // Define type mapping
%pointer_class(...) // Define pointer class
%array_class(...) // Define array class
Base Class Implementation
All adapters must inherit from the CanBase
abstract class and implement the following required methods:
abstract class CanBase {
abstract info: CanBaseInfo; // Adapter basic information
abstract log: CanLOG; // Logging object
abstract close(): void; // Close the adapter
abstract readBase(...): Promise<...>; // Read CAN message
abstract writeBase(...): Promise<...>;// Write CAN message
abstract getReadBaseId(...): string; // Get read ID
abstract setOption(...): void; // Set options
abstract event: EventEmitter; // Event emitter
}
Required Static Methods
Each adapter class must implement the following static methods:
getValidDevices()
: Returns the list of available devicesgetLibVersion()
: Returns the library version informationgetDefaultBitrate()
: Returns the default bitrate configuration
Implementation Steps
- Create adapter directory structure
- Implement SWIG interface to wrap native libraries
- Create adapter class and inherit from
CanBase
- Implement all required abstract methods
- Implement static methods
- Add error handling and logging
- Implement event handling mechanism
- Add proper cleanup in close() method
- Implement proper error propagation
Example
Reference implementation from the kvaser
adapter:
export class KVASER_CAN extends CanBase {
event: EventEmitter;
info: CanBaseInfo;
handle: number;
private closed: boolean = false;
private readAbort: AbortController;
constructor(info: CanBaseInfo) {
super();
this.info = info;
this.event = new EventEmitter();
this.readAbort = new AbortController();
// Initialize native resources
}
async readBase(id: number, msgType: CanMsgType, timeout: number): Promise<{ data: Buffer; ts: number }> {
// Implementation
}
async writeBase(id: number, msgType: CanMsgType, data: Buffer): Promise<number> {
// Implementation
}
close(): void {
if (this.closed) return;
this.closed = true;
this.readAbort.abort();
// Cleanup native resources
}
// ... other methods
}
Testing
After implementing the adapter, you should create a test file(${adapter_name}.test.ts
) in the test/docan
directory. Here's how to implement the tests:
1. Test Setup
import { describe, it, beforeAll, afterAll, test } from 'vitest'
import * as path from 'path'
// Load native library
const dllPath = path.join(__dirname, '../../resources/lib')
YOUR_ADAPTER.loadDllPath(dllPath)
describe('adapter test', () => {
let client!: YOUR_ADAPTER
beforeAll(() => {
// Initialize adapter with test configuration
client = new YOUR_ADAPTER({
handle: 0,
name: 'test',
id: 'adapter_name',
vendor: 'vendor_name',
canfd: false,
bitrate: {
freq: 250000,
preScaler: 2,
timeSeg1: 68,
timeSeg2: 11,
sjw: 11
},
bitratefd: {
freq: 1000000,
preScaler: 1,
timeSeg1: 20,
timeSeg2: 19,
sjw: 19
}
})
})
afterAll(() => {
// Clean up
client.close()
})
})
2. Test Cases
Implement the following test cases:
Basic Operations
typescripttest('basic operations', async () => { // Test adapter initialization expect(client.info).toBeDefined() expect(client.info.name).toBe('test') // Test device capabilities const devices = await YOUR_ADAPTER.getValidDevices() expect(devices.length).toBeGreaterThan(0) // Test version information const version = await YOUR_ADAPTER.getLibVersion() expect(version).toBeDefined() })
Message Transmission
typescripttest('message transmission', async () => { // Test single frame transmission const result = await client.writeBase( 3, // CAN ID { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, Buffer.alloc(8, 0) ) expect(result).toBeDefined() // Test read operation const readResult = await client.readBase(3, { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, 1000) expect(readResult).toBeDefined() expect(readResult.data).toBeDefined() })
Multi-frame Transmission
typescripttest('multi-frame transmission', async () => { const list = [] for (let i = 0; i < 10; i++) { list.push( client.writeBase( 3, { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, Buffer.alloc(8, i) ) ) } const results = await Promise.all(list) expect(results.length).toBe(10) expect(results.every(r => r !== undefined)).toBe(true) })
Error Handling
typescripttest('error handling', async () => { // Test invalid parameters await expect(client.writeBase( -1, // Invalid CAN ID { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, Buffer.alloc(8, 0) )).rejects.toThrow() // Test closed adapter client.close() await expect(client.writeBase( 3, { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, Buffer.alloc(8, 0) )).rejects.toThrow() })
Event Handling
typescripttest('event handling', async () => { const messageHandler = jest.fn() client.attachCanMessage(messageHandler) // Trigger some events await client.writeBase(3, { idType: CAN_ID_TYPE.STANDARD, brs: false, canfd: false, remote: false }, Buffer.alloc(8, 0)) // Verify event handling expect(messageHandler).toHaveBeenCalled() client.detachCanMessage(messageHandler) })
3. Test Configuration
Make sure to test with different configurations:
- Standard CAN vs CAN FD
- Different bitrates
- Different message types (standard/extended)
- Different data lengths
- Error conditions and recovery
- Timeout scenarios
- Resource cleanup
4. Running Tests
Run the tests using:
npm test
# or
vitest test/docan/${adapter_name}.test.ts
Remember to:
- Test on different platforms if your adapter supports multiple platforms
- Test with different hardware configurations
- Test error conditions and recovery scenarios
- Test performance with high message rates
- Test long-running operations for stability
- Test resource cleanup and memory leaks
- Test concurrent operations
- Test timeout handling