Initial commit
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
/* Makita LXT module, ported from the Python desktop app (modules/makita_lxt.py).
|
||||
The widget renders the results table and function-test buttons; the host
|
||||
page drives the reading via readBattery(). */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const { debug, hexDump, modal, toast, ConnectionError } = OBI.util;
|
||||
|
||||
/* Command definitions (start, len, rsp_len, cmd, data...) */
|
||||
const MODEL_CMD = [0x01, 0x02, 0x10, 0xcc, 0xdc, 0x0c];
|
||||
const READ_DATA_REQUEST = [0x01, 0x04, 0x1d, 0xcc, 0xd7, 0x00, 0x00, 0xff];
|
||||
const TESTMODE_CMD = [0x01, 0x03, 0x09, 0x33, 0xd9, 0x96, 0xa5];
|
||||
const LEDS_ON_CMD = [0x01, 0x02, 0x09, 0x33, 0xda, 0x31];
|
||||
const LEDS_OFF_CMD = [0x01, 0x02, 0x09, 0x33, 0xda, 0x34];
|
||||
const RESET_ERROR_CMD = [0x01, 0x02, 0x09, 0x33, 0xda, 0x04];
|
||||
const READ_MSG_CMD = [0x01, 0x02, 0x28, 0x33, 0xaa, 0x00];
|
||||
const CLEAR_CMD = [0x01, 0x02, 0x00, 0xcc, 0xf0, 0x00];
|
||||
|
||||
/* Commands specific to the F0513 version */
|
||||
const F0513_VCELL_1_CMD = [0x01, 0x01, 0x02, 0xcc, 0x31];
|
||||
const F0513_VCELL_2_CMD = [0x01, 0x01, 0x02, 0xcc, 0x32];
|
||||
const F0513_VCELL_3_CMD = [0x01, 0x01, 0x02, 0xcc, 0x33];
|
||||
const F0513_VCELL_4_CMD = [0x01, 0x01, 0x02, 0xcc, 0x34];
|
||||
const F0513_VCELL_5_CMD = [0x01, 0x01, 0x02, 0xcc, 0x35];
|
||||
const F0513_TEMP_CMD = [0x01, 0x01, 0x02, 0xcc, 0x52];
|
||||
const F0513_MODEL_CMD = [0x01, 0x00, 0x02, 0x31];
|
||||
const F0513_VERSION_CMD = [0x01, 0x00, 0x02, 0x32];
|
||||
const F0513_TESTMODE_CMD = [0x01, 0x01, 0x00, 0xcc, 0x99];
|
||||
|
||||
const initialData = {
|
||||
"Model": "",
|
||||
"Charge count*": "",
|
||||
"State": "",
|
||||
"Status code": "",
|
||||
"Pack Voltage": "",
|
||||
"Cell 1 Voltage": "",
|
||||
"Cell 2 Voltage": "",
|
||||
"Cell 3 Voltage": "",
|
||||
"Cell 4 Voltage": "",
|
||||
"Cell 5 Voltage": "",
|
||||
"Cell Voltage Difference": "",
|
||||
"Temperature Sensor 1": "",
|
||||
"Temperature Sensor 2": "",
|
||||
"ROM ID": "",
|
||||
"Manufacturing date": "",
|
||||
"Battery message": "",
|
||||
"Capacity": "",
|
||||
"Battery type": "",
|
||||
};
|
||||
|
||||
const nibbleSwap = (byte) =>
|
||||
(((byte & 0xf0) >> 4) | ((byte & 0x0f) << 4)) & 0xff;
|
||||
|
||||
const u16le = (bytes, offset) =>
|
||||
(bytes[offset] | (bytes[offset + 1] << 8)) & 0xffff;
|
||||
|
||||
function createWidget(container) {
|
||||
container.innerHTML =
|
||||
'<div class="test-grid">' +
|
||||
"<fieldset class='test-group'><legend>Function test</legend>" +
|
||||
'<button data-action="led-on" disabled>LED test ON</button>' +
|
||||
'<button data-action="led-off" disabled>LED test OFF</button>' +
|
||||
"</fieldset>" +
|
||||
"<fieldset class='test-group'><legend>Reset battery</legend>" +
|
||||
'<button data-action="reset-errors" disabled>Clear errors</button>' +
|
||||
'<button data-action="reset-message" disabled>Reset battery message</button>' +
|
||||
"</fieldset>" +
|
||||
"</div>" +
|
||||
'<div class="table-wrap"><table id="data-table">' +
|
||||
"<thead><tr><th>Parameter</th><th>Value</th></tr></thead>" +
|
||||
"<tbody></tbody></table></div>" +
|
||||
'<div class="table-actions">' +
|
||||
'<button data-action="copy">Copy</button>' +
|
||||
'<button data-action="clear">Clear</button>' +
|
||||
"</div>";
|
||||
|
||||
const state = {
|
||||
interface: null,
|
||||
commandVersion: null,
|
||||
batteryPresent: false,
|
||||
busy: false,
|
||||
};
|
||||
|
||||
const buttons = {
|
||||
"led-on": container.querySelector('[data-action="led-on"]'),
|
||||
"led-off": container.querySelector('[data-action="led-off"]'),
|
||||
"reset-errors": container.querySelector('[data-action="reset-errors"]'),
|
||||
"reset-message": container.querySelector('[data-action="reset-message"]'),
|
||||
};
|
||||
|
||||
const enableAllButtons = () => {
|
||||
for (const key in buttons) buttons[key].disabled = false;
|
||||
};
|
||||
|
||||
const tbody = container.querySelector("tbody");
|
||||
const rows = {}; /* parameter -> <tr> */
|
||||
|
||||
function insertBatteryData(data) {
|
||||
for (const [parameter, value] of Object.entries(data)) {
|
||||
let tr = rows[parameter];
|
||||
if (tr) {
|
||||
tr.querySelector("td.value").textContent = String(value);
|
||||
} else {
|
||||
tr = document.createElement("tr");
|
||||
const tdParam = document.createElement("td");
|
||||
tdParam.textContent = parameter;
|
||||
const tdValue = document.createElement("td");
|
||||
tdValue.className = "value";
|
||||
tdValue.textContent = String(value);
|
||||
tr.appendChild(tdParam);
|
||||
tr.appendChild(tdValue);
|
||||
tr.addEventListener("click", () => tr.classList.toggle("selected"));
|
||||
tbody.appendChild(tr);
|
||||
rows[parameter] = tr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getInterface = () => {
|
||||
if (!state.interface) {
|
||||
modal(
|
||||
"Error",
|
||||
"No interface selected. Please select and connect an interface from the sidebar."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return state.interface;
|
||||
};
|
||||
|
||||
/* ---- model identification ---- */
|
||||
async function getModel() {
|
||||
const response = await state.interface.request(MODEL_CMD);
|
||||
return String.fromCharCode(...response.slice(2, 9));
|
||||
}
|
||||
|
||||
async function getF0513Model() {
|
||||
const response = await state.interface.request(F0513_MODEL_CMD);
|
||||
await state.interface.request(CLEAR_CMD);
|
||||
state.commandVersion = "F0513";
|
||||
modal("Limited", "This model only supports diagnostics");
|
||||
return (
|
||||
"BL" +
|
||||
response[2].toString(16).toUpperCase() +
|
||||
response[3].toString(16).toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- dynamic data ---- */
|
||||
async function readDynamicData() {
|
||||
let batteryData;
|
||||
if (state.commandVersion === "F0513") {
|
||||
await state.interface.request(CLEAR_CMD);
|
||||
await state.interface.request(CLEAR_CMD);
|
||||
const cellCmds = [
|
||||
F0513_VCELL_1_CMD,
|
||||
F0513_VCELL_2_CMD,
|
||||
F0513_VCELL_3_CMD,
|
||||
F0513_VCELL_4_CMD,
|
||||
F0513_VCELL_5_CMD,
|
||||
];
|
||||
const responses = [];
|
||||
for (const cmd of cellCmds) {
|
||||
responses.push(await state.interface.request(cmd));
|
||||
}
|
||||
const temp = await state.interface.request(F0513_TEMP_CMD);
|
||||
const voltages = responses.map((r) => u16le(r, 2) / 1000);
|
||||
const vPack = voltages.reduce((a, b) => a + b, 0);
|
||||
const vDiff = Math.round((Math.max(...voltages) - Math.min(...voltages)) * 100) / 100;
|
||||
const tCell = u16le(temp, 2) / 100;
|
||||
batteryData = {
|
||||
"Pack Voltage": vPack,
|
||||
"Cell 1 Voltage": voltages[0],
|
||||
"Cell 2 Voltage": voltages[1],
|
||||
"Cell 3 Voltage": voltages[2],
|
||||
"Cell 4 Voltage": voltages[3],
|
||||
"Cell 5 Voltage": voltages[4],
|
||||
"Cell Voltage Difference": vDiff,
|
||||
"Temperature Sensor 1": tCell,
|
||||
"Temperature Sensor 2": "",
|
||||
};
|
||||
} else {
|
||||
const response = await state.interface.request(READ_DATA_REQUEST);
|
||||
const vPack = u16le(response, 2) / 1000;
|
||||
const voltages = [4, 6, 8, 10, 12].map((o) => u16le(response, o) / 1000);
|
||||
const vDiff = Math.round((Math.max(...voltages) - Math.min(...voltages)) * 100) / 100;
|
||||
const tCell = u16le(response, 16) / 100;
|
||||
const tMosfet = u16le(response, 18) / 100;
|
||||
batteryData = {
|
||||
"Pack Voltage": vPack,
|
||||
"Cell 1 Voltage": voltages[0],
|
||||
"Cell 2 Voltage": voltages[1],
|
||||
"Cell 3 Voltage": voltages[2],
|
||||
"Cell 4 Voltage": voltages[3],
|
||||
"Cell 5 Voltage": voltages[4],
|
||||
"Cell Voltage Difference": vDiff,
|
||||
"Temperature Sensor 1": tCell,
|
||||
"Temperature Sensor 2": tMosfet,
|
||||
};
|
||||
}
|
||||
return batteryData;
|
||||
}
|
||||
|
||||
/* ---- combined read: static info + model + dynamic data ---- */
|
||||
async function readBattery() {
|
||||
const iface = getInterface();
|
||||
if (!iface) return false;
|
||||
if (state.busy) return false;
|
||||
state.busy = true;
|
||||
try {
|
||||
/* static info */
|
||||
let response;
|
||||
try {
|
||||
response = await iface.request(READ_MSG_CMD);
|
||||
} catch (e) {
|
||||
if (e instanceof ConnectionError) {
|
||||
modal(
|
||||
"Connection Error",
|
||||
"Could not communicate with the battery:\n\n" + e.message
|
||||
);
|
||||
} else {
|
||||
modal(
|
||||
"Data Error",
|
||||
"Received an unexpected response while reading battery info:\n\n" +
|
||||
e.name + ": " + e.message
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const romId = hexDump(response.slice(2, 10));
|
||||
const rawMsg = hexDump(response.slice(10, 42));
|
||||
const swapped = [nibbleSwap(response[36]), nibbleSwap(response[37])];
|
||||
const chargeCount = (((swapped[0] << 8) | swapped[1]) & 0x0fff) >>> 0;
|
||||
const lockNibble = response[30] & 0x0f;
|
||||
const errorByte = response[29];
|
||||
const lockStatus = lockNibble > 0 ? "LOCKED" : "UNLOCKED";
|
||||
|
||||
const pad2 = (b) => (b & 0xff).toString(10).padStart(2, "0");
|
||||
const hex2 = (b) => (b & 0xff).toString(16).toUpperCase().padStart(2, "0");
|
||||
insertBatteryData({
|
||||
"ROM ID": romId,
|
||||
"Battery message": rawMsg,
|
||||
"Charge count*": chargeCount,
|
||||
"State": lockStatus,
|
||||
"Status code": hex2(errorByte),
|
||||
"Manufacturing date":
|
||||
pad2(response[4]) + "/" + pad2(response[3]) + "/20" + pad2(response[2]),
|
||||
"Capacity": (nibbleSwap(response[26]) / 10).toFixed(1) + "Ah",
|
||||
"Battery type": nibbleSwap(response[21]),
|
||||
});
|
||||
state.batteryPresent = true;
|
||||
|
||||
/* model identification */
|
||||
state.commandVersion = null;
|
||||
let model = null;
|
||||
let lastException = null;
|
||||
for (const command of [getModel, getF0513Model]) {
|
||||
try {
|
||||
model = await command();
|
||||
break;
|
||||
} catch (e) {
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
if (model === null) {
|
||||
modal(
|
||||
"Unsupported Battery",
|
||||
"Battery is present but the model is not supported.\n\nLast error: " +
|
||||
(lastException && lastException.message)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
insertBatteryData({ Model: model });
|
||||
|
||||
/* dynamic data */
|
||||
try {
|
||||
const batteryData = await readDynamicData();
|
||||
insertBatteryData(batteryData);
|
||||
} catch (e) {
|
||||
if (e instanceof ConnectionError) {
|
||||
modal(
|
||||
"Connection Error",
|
||||
"Lost communication while reading battery data:\n\n" + e.message
|
||||
);
|
||||
} else {
|
||||
modal(
|
||||
"Data Error",
|
||||
"Received an unexpected response while reading battery data:\n\n" +
|
||||
e.name + ": " + e.message
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
enableAllButtons();
|
||||
return true;
|
||||
} finally {
|
||||
state.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- function test ---- */
|
||||
async function onAllLedsOnClick() {
|
||||
const iface = getInterface();
|
||||
if (!iface) return;
|
||||
try {
|
||||
await iface.request(TESTMODE_CMD);
|
||||
await iface.request(LEDS_ON_CMD);
|
||||
toast("LED test on — check the battery LEDs");
|
||||
} catch (e) {
|
||||
modal(
|
||||
"Connection Error",
|
||||
"Lost communication while turning LEDs on:\n\n" + e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAllLedsOffClick() {
|
||||
const iface = getInterface();
|
||||
if (!iface) return;
|
||||
try {
|
||||
if (state.commandVersion === "F0513") {
|
||||
await iface.request(F0513_TESTMODE_CMD);
|
||||
} else {
|
||||
await iface.request(TESTMODE_CMD);
|
||||
}
|
||||
await iface.request(LEDS_OFF_CMD);
|
||||
toast("LED test off");
|
||||
} catch (e) {
|
||||
modal(
|
||||
"Connection Error",
|
||||
"Lost communication while turning LEDs off:\n\n" + e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- reset ---- */
|
||||
async function onResetErrorsClick() {
|
||||
const iface = getInterface();
|
||||
if (!iface) return;
|
||||
try {
|
||||
await iface.request(TESTMODE_CMD);
|
||||
await iface.request(RESET_ERROR_CMD);
|
||||
toast("Battery errors cleared");
|
||||
} catch (e) {
|
||||
modal(
|
||||
"Connection Error",
|
||||
"Lost communication while resetting errors:\n\n" + e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onResetMessageClick() {
|
||||
const iface = getInterface();
|
||||
if (!iface) return;
|
||||
await modal(
|
||||
"Not Implemented",
|
||||
"This feature is currently under development."
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- table actions ---- */
|
||||
function copyToClipboard() {
|
||||
const selected = Array.from(tbody.querySelectorAll("tr.selected"));
|
||||
if (!selected.length) {
|
||||
modal("No Selection", "No rows selected to copy!");
|
||||
return;
|
||||
}
|
||||
const rowsText = selected
|
||||
.map((tr) => {
|
||||
const cells = tr.querySelectorAll("td");
|
||||
return Array.from(cells)
|
||||
.map((td) => td.textContent)
|
||||
.join("\t");
|
||||
})
|
||||
.join("\n");
|
||||
navigator.clipboard
|
||||
.writeText(rowsText)
|
||||
.then(() => toast("Rows copied to clipboard"))
|
||||
.catch(() => modal("Error", "Could not access the clipboard."));
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
insertBatteryData(initialData);
|
||||
}
|
||||
|
||||
container
|
||||
.querySelector('[data-action="led-on"]')
|
||||
.addEventListener("click", onAllLedsOnClick);
|
||||
container
|
||||
.querySelector('[data-action="led-off"]')
|
||||
.addEventListener("click", onAllLedsOffClick);
|
||||
container
|
||||
.querySelector('[data-action="reset-errors"]')
|
||||
.addEventListener("click", onResetErrorsClick);
|
||||
container
|
||||
.querySelector('[data-action="reset-message"]')
|
||||
.addEventListener("click", onResetMessageClick);
|
||||
container
|
||||
.querySelector('[data-action="copy"]')
|
||||
.addEventListener("click", copyToClipboard);
|
||||
container
|
||||
.querySelector('[data-action="clear"]')
|
||||
.addEventListener("click", clearData);
|
||||
|
||||
insertBatteryData(initialData);
|
||||
|
||||
return {
|
||||
setInterface(iface) {
|
||||
state.interface = iface;
|
||||
},
|
||||
readBattery,
|
||||
destroy() {
|
||||
container.innerHTML = "";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
OBI.modules["makita_lxt"] = {
|
||||
getDisplayName: () => "Makita LXT",
|
||||
createWidget,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user