diff --git a/serial/src/lib/TemperatureSensor.svelte b/serial/src/lib/TemperatureSensor.svelte index 14f4685..c2361ab 100644 --- a/serial/src/lib/TemperatureSensor.svelte +++ b/serial/src/lib/TemperatureSensor.svelte @@ -182,12 +182,11 @@
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. Holding Registers - + 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); +} diff --git a/serial/src/lib/temperatureSensor.ts b/serial/src/lib/temperatureSensor.ts index 4635213..671d505 100644 --- a/serial/src/lib/temperatureSensor.ts +++ b/serial/src/lib/temperatureSensor.ts @@ -1,6 +1,10 @@ -import type { BaudRate } from "./BaudRateSelect.svelte"; import { crc16 } from "./crc"; -import { read, write } from "./serial"; +import { + FunctionCode, + readHoldingRegister, + readInputRegister, + writeHoldingRegister, +} from "./modbus"; export type SensorState = | "not connected" @@ -15,11 +19,9 @@ export type SensorState = humidityCorrection?: number; }; -enum FunctionCode { - ReadHoldingRegister = 0x03, - ReadInputRegister = 0x04, - WriteSingleHoldingRegister = 0x06, - WriteMultipleHoldingRegisters = 0x10, +export function isValidAddress(value: any): value is number { + // Device address (1~247) + return Number.isInteger(value) && value < 248 && value > 0; } /** @@ -54,53 +56,15 @@ export async function readTemperature( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadInputRegister); - // Register address - view.setUint16(2, InputRegisterAddress.Temperature); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadInputRegister, 2)) - return; + const response = await readInputRegister( + port, + address, + { address: InputRegisterAddress.Temperature, length: 2 }, + signal + ); - const temperature = new DataView(value.buffer).getUint16(3); - return temperature / 10; + if (!response) return; + return response.getUint16(0) / 10; } export async function readHumidity( @@ -108,53 +72,15 @@ export async function readHumidity( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadInputRegister); - // Register address - view.setUint16(2, InputRegisterAddress.Humidity); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadInputRegister, 2)) - return; + const response = await readInputRegister( + port, + address, + { address: InputRegisterAddress.Humidity, length: 2 }, + signal + ); - const humidity = new DataView(value.buffer).getUint16(3); - return humidity / 10; + if (!response) return; + return response.getUint16(0) / 10; } export async function readAddress( @@ -162,53 +88,15 @@ export async function readAddress( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.DeviceAddress); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadHoldingRegister, 2)) - return; + const response = await readHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.DeviceAddress, length: 2 }, + signal + ); - const deviceAddress = new DataView(value.buffer).getUint16(3); - return deviceAddress; + if (!response) return; + return response.getUint16(0); } export async function readBaudRate( @@ -216,53 +104,15 @@ export async function readBaudRate( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.BaudRate); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadHoldingRegister, 2)) - return; + const response = await readHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.BaudRate, length: 2 }, + signal + ); - const baudRateKey = new DataView(value.buffer).getUint16(3); - return baudRateKey; + if (!response) return; + return response.getUint16(0); } export async function readTemperatureCorrection( @@ -270,53 +120,15 @@ export async function readTemperatureCorrection( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.TemperatureCorrectionValue); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadHoldingRegister, 2)) - return; + const response = await readHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.TemperatureCorrectionValue, length: 2 }, + signal + ); - const correctionValue = new DataView(value.buffer).getInt16(3); - return correctionValue / 10; + if (!response) return; + return response.getInt16(0) / 10; } export async function readHumidityCorrection( @@ -324,96 +136,15 @@ export async function readHumidityCorrection( address: number, signal: AbortSignal ) { - if ( - signal.aborted || - !isValidAddress(address) || - !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, address); - // Function - view.setUint8(1, FunctionCode.ReadHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.HumidityCorrectionValue); - // 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; - - if (!isReadResponseValid(value, address, FunctionCode.ReadHoldingRegister, 2)) - return; - - const correctionValue = new DataView(value.buffer).getInt16(3); - return correctionValue / 10; -} - -/** - * Validates a response to determine that the response is intended for us as it is a shared bus medium - */ -function isReadResponseValid( - responseView: Uint8Array, - expectedAddress: number, - expectedFunctionCode: FunctionCode, - expectedDataLength: number -) { - const view = new DataView(responseView.buffer); - const crc = crc16(responseView.subarray(0, responseView.length - 2)); - const expectedCrc = view.getUint16(5); - - if (crc !== expectedCrc) return false; - - const slaveAddress = view.getUint8(0); - if (expectedAddress !== slaveAddress) return false; - - const functionCode = view.getUint8(1); - if (functionCode !== expectedFunctionCode) return false; - - const dataLength = view.getUint8(2); - return dataLength === expectedDataLength; -} - -/** - * 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; -} + const response = await readHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.HumidityCorrectionValue, length: 2 }, + signal + ); -export function isValidAddress(value: any): value is number { - // Device address (1~247) - return Number.isInteger(value) && value < 248 && value > 0; + if (!response) return; + return response.getInt16(0) / 10; } export function isValidBaudRate( @@ -430,49 +161,15 @@ export async function writeTemperatureCorrection( correction: number, signal: AbortSignal ): Promise { - if ( - signal.aborted || - !isValidAddress(address) || - !isValidCorrectionValue(correction) - ) - return false; - - const length = 8; - const crcOffset = length - 2; - const frame = new Uint8Array(length); - const view = new DataView(frame.buffer); - // Address - view.setUint8(0, address); - // Function - view.setUint8(1, FunctionCode.WriteSingleHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.TemperatureCorrectionValue); - // Register value - view.setInt16(4, correction * 10); - // 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 value; - try { - value = await read(port, signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return false; - } - - if (!value) return false; - - if (!isWriteResponseValid(frame, value)) return false; - - return true; + if (signal.aborted || !isValidCorrectionValue(correction)) return false; + + return await writeHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.TemperatureCorrectionValue, length: 2 }, + correction * 10, + signal + ); } export async function writeHumidityCorrection( @@ -481,49 +178,15 @@ export async function writeHumidityCorrection( correction: number, signal: AbortSignal ): Promise { - if ( - signal.aborted || - !isValidAddress(address) || - !isValidCorrectionValue(correction) - ) - return false; - - const length = 8; - const crcOffset = length - 2; - const frame = new Uint8Array(length); - const view = new DataView(frame.buffer); - // Address - view.setUint8(0, address); - // Function - view.setUint8(1, FunctionCode.WriteSingleHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.HumidityCorrectionValue); - // Register value - view.setInt16(4, correction * 10); - // 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 value; - try { - value = await read(port, signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return false; - } - - if (!value) return false; - - if (!isWriteResponseValid(frame, value)) return false; - - return true; + if (signal.aborted || !isValidCorrectionValue(correction)) return false; + + return await writeHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.HumidityCorrectionValue, length: 2 }, + correction * 10, + signal + ); } export async function writeAddress( @@ -532,45 +195,15 @@ export async function writeAddress( newAddress: number, signal: AbortSignal ): Promise { - if (signal.aborted || !isValidAddress(address) || !isValidAddress(newAddress)) - return false; - - const length = 8; - const crcOffset = length - 2; - const frame = new Uint8Array(length); - const view = new DataView(frame.buffer); - // Address - view.setUint8(0, address); - // Function - view.setUint8(1, FunctionCode.WriteSingleHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.DeviceAddress); - // Register value - view.setUint16(4, newAddress); - // 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 value; - try { - value = await read(port, signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return false; - } - - if (!value) return false; - - if (!isWriteResponseValid(frame, value)) return false; - - return true; + if (signal.aborted || !isValidAddress(newAddress)) return false; + + return await writeHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.DeviceAddress, length: 2 }, + newAddress, + signal + ); } export async function writeBaudRate( @@ -579,43 +212,13 @@ export async function writeBaudRate( baudRate: TemperatureSensorBaudRate, signal: AbortSignal ): Promise { - if (signal.aborted || !isValidAddress(address) || !isValidBaudRate(baudRate)) - return false; - - const length = 8; - const crcOffset = length - 2; - const frame = new Uint8Array(length); - const view = new DataView(frame.buffer); - // Address - view.setUint8(0, address); - // Function - view.setUint8(1, FunctionCode.WriteSingleHoldingRegister); - // Register address - view.setUint16(2, HoldingRegisterAddress.BaudRate); - // Register value - view.setUint16(4, baudRate); - // CRC - const crc = crc16(frame.subarray(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 value; - try { - value = await read(port, signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return false; - } - - if (!value) return false; - - if (!isWriteResponseValid(frame, value)) return false; - - return true; + if (signal.aborted || !isValidBaudRate(baudRate)) return false; + + return await writeHoldingRegister( + port, + address, + { address: HoldingRegisterAddress.BaudRate, length: 2 }, + baudRate, + signal + ); }