Skip to content

Commit

Permalink
Refactor operations to simplify writing device specific implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
SantaClaas committed Sep 17, 2023
1 parent a432ee6 commit 12fde5d
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 494 deletions.
7 changes: 3 additions & 4 deletions src/lib/TemperatureSensor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,11 @@
<fieldset class="holding-registers" disabled={isBlocked}>
<span>
Note: The active device address and baud rate is read from holding register
to input register on power up. Therefore after wrting to the holding
register the input address does not change until the device is turned off
and on again.
register after device is powered on. Therefore changing these values does
not apply until after next power cycle.
</span>
<legend>Holding Registers</legend>
<label for="device-address-register">Address </label>
<label for="device-address-register">Address</label>
<input
id="device-address-register"
type="number"
Expand Down
213 changes: 213 additions & 0 deletions src/lib/modbus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { crc16 } from "./crc";
import { read, write } from "./serial";

export enum FunctionCode {
ReadHoldingRegister = 0x03,
ReadInputRegister = 0x04,
WriteSingleHoldingRegister = 0x06,
Diagnostics = 0x08,
WriteMultipleHoldingRegisters = 0x10,
}

/**
* Validates that the given value is a valid device address between 1 and 247
*/
function isValidAddress(value: any): value is number {
// Device address (1-247), 0 is broadcast
return Number.isInteger(value) && value < 248 && value > 0;
}

/**
* Validates a response to determine that the response is intended for us as it is a shared bus medium
*/
function isReadResponseValid(
response: { view: DataView; frame: Uint8Array },
expectedAddress: number,
expectedFunctionCode: FunctionCode,
expectedDataLength: number
) {
const crcStartIndex = response.frame.byteLength - 2;
const crc = crc16(response.frame.subarray(0, crcStartIndex));
const expectedCrc = response.view.getUint16(5);

if (crc !== expectedCrc) return false;

const slaveAddress = response.view.getUint8(0);
if (expectedAddress !== slaveAddress) return false;

const functionCode = response.view.getUint8(1);
if (functionCode !== expectedFunctionCode) return false;

const dataLength = response.view.getUint8(2);
return dataLength === expectedDataLength && 2 + dataLength < crcStartIndex;
}

/**
* Validates that the response to a write is valid, that is the response mirrors the command
*/
function isWriteResponseValid(frame: Uint8Array, response: Uint8Array) {
if (frame.length !== response.length) return false;

for (let index = 0; index < frame.length; index++) {
if (frame[index] !== response[index]) return false;
}

return true;
}

type Register = {
/**
* A valid register address is a unsigned 16 bit number
*/
address: number;
/**
* The length in bytes of the data contained in the register
*/
length: number;
};

async function readRegister(
port: SerialPort,
deviceAddress: number,
register: Register,
registerType:
| FunctionCode.ReadInputRegister
| FunctionCode.ReadHoldingRegister,
signal: AbortSignal
) {
if (
signal.aborted ||
!isValidAddress(deviceAddress) ||
!port.writable ||
!port.readable
)
return;

const length = 8;
const crcOffset = length - 2;
const frame = new ArrayBuffer(length);

const view = new DataView(frame);
// Address
view.setUint8(0, deviceAddress);
// Function
view.setUint8(1, registerType);
// Register address
view.setUint16(2, register.address);
// Number of registers
view.setUint16(4, 1);
// CRC
const crc = crc16(new Uint8Array(frame.slice(0, crcOffset)));
view.setUint16(crcOffset, crc);

try {
await write(port, frame, signal);
} catch (error: unknown) {
console.warn("Non-fatal write error:", error);
return;
}

let value;
try {
value = await read(port, signal);
} catch (error: unknown) {
console.warn("Non-fatal read error:", error);
return;
}

if (!value) return;

const response = { view: new DataView(value.buffer), frame: value };
if (
!isReadResponseValid(response, deviceAddress, registerType, register.length)
)
return;

// Length is already validated at this point
// Interpretation of data is left to caller
return new DataView(response.frame.buffer, 3, register.length);
}

export function readInputRegister(
port: SerialPort,
deviceAddress: number,
register: Register,
signal: AbortSignal
) {
return readRegister(
port,
deviceAddress,
register,
FunctionCode.ReadInputRegister,
signal
);
}

export function readHoldingRegister(
port: SerialPort,
deviceAddress: number,
register: Register,
signal: AbortSignal
) {
return readRegister(
port,
deviceAddress,
register,
FunctionCode.ReadHoldingRegister,
signal
);
}

/**
*
* @param port The serial port used to write to write the holding register on the modbus device
* @param deviceAddress the 8 bit modbus device address
* @param register the object describing the holding register
* @param value the value to write to the holding register. Only 16 bit signed integers are currently supported.
* @param signal the abort signal to cancel the writing
* @returns true if the value was written successfully, else false
*/
export async function writeHoldingRegister(
port: SerialPort,
deviceAddress: number,
register: Register,
value: number,
signal: AbortSignal
) {
if (signal.aborted || !isValidAddress(deviceAddress)) return false;

const length = 8;
const crcOffset = length - 2;
const frame = new Uint8Array(length);
const view = new DataView(frame.buffer);
// Address
view.setUint8(0, deviceAddress);
// Function
view.setUint8(1, FunctionCode.WriteSingleHoldingRegister);
// Register address
view.setUint16(2, register.address);
// Register value
view.setInt16(4, value);
// CRC
const crc = crc16(new Uint8Array(frame.slice(0, crcOffset)));
view.setUint16(crcOffset, crc);

try {
await write(port, frame, signal);
} catch (error: unknown) {
console.warn("Non-fatal write error:", error);
return false;
}

let responseFrame;
try {
responseFrame = await read(port, signal);
} catch (error: unknown) {
console.warn("Non-fatal read error:", error);
return false;
}

if (!responseFrame) return false;

return isWriteResponseValid(frame, responseFrame);
}
Loading

0 comments on commit 12fde5d

Please sign in to comment.