Align speaker config server response
This commit is contained in:
@ -170,6 +170,9 @@ QF100_MQTT_BROKER_PORT=8883
|
|||||||
QF100_MQTT_USERNAME=qris-backend
|
QF100_MQTT_USERNAME=qris-backend
|
||||||
QF100_MQTT_PASSWORD=CHANGE_ME_MQTT_BACKEND_PASSWORD
|
QF100_MQTT_PASSWORD=CHANGE_ME_MQTT_BACKEND_PASSWORD
|
||||||
QF100_MQTT_KEEP_ALIVE_SECONDS=60
|
QF100_MQTT_KEEP_ALIVE_SECONDS=60
|
||||||
|
QF100_APP_CONFIG_VERSION=21
|
||||||
|
QF100_DYNAMIC_QR_URL=
|
||||||
|
QF100_DYNAMIC_QR_GAP_SECONDS=0
|
||||||
|
|
||||||
DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED=true
|
DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED=true
|
||||||
DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS=60000
|
DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS=60000
|
||||||
|
|||||||
@ -40,6 +40,9 @@ export const env = {
|
|||||||
QF100_MQTT_USERNAME: process.env.QF100_MQTT_USERNAME || "",
|
QF100_MQTT_USERNAME: process.env.QF100_MQTT_USERNAME || "",
|
||||||
QF100_MQTT_PASSWORD: process.env.QF100_MQTT_PASSWORD || "",
|
QF100_MQTT_PASSWORD: process.env.QF100_MQTT_PASSWORD || "",
|
||||||
QF100_MQTT_KEEP_ALIVE_SECONDS: Number(process.env.QF100_MQTT_KEEP_ALIVE_SECONDS || 60),
|
QF100_MQTT_KEEP_ALIVE_SECONDS: Number(process.env.QF100_MQTT_KEEP_ALIVE_SECONDS || 60),
|
||||||
|
QF100_APP_CONFIG_VERSION: Number(process.env.QF100_APP_CONFIG_VERSION || 0),
|
||||||
|
QF100_DYNAMIC_QR_URL: process.env.QF100_DYNAMIC_QR_URL || "",
|
||||||
|
QF100_DYNAMIC_QR_GAP_SECONDS: Number(process.env.QF100_DYNAMIC_QR_GAP_SECONDS || 0),
|
||||||
DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED: process.env.DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED || "true",
|
DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED: process.env.DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED || "true",
|
||||||
DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS: Number(process.env.DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS || 60000),
|
DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS: Number(process.env.DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS || 60000),
|
||||||
DYNAMIC_QR_EXPIRY_SWEEP_LIMIT: Number(process.env.DYNAMIC_QR_EXPIRY_SWEEP_LIMIT || 100),
|
DYNAMIC_QR_EXPIRY_SWEEP_LIMIT: Number(process.env.DYNAMIC_QR_EXPIRY_SWEEP_LIMIT || 100),
|
||||||
|
|||||||
@ -18,6 +18,16 @@ type Qf100ConfigRequest = {
|
|||||||
iccid?: string;
|
iccid?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SpeakerReportRequest = Qf100ConfigRequest & {
|
||||||
|
result?: number;
|
||||||
|
"success-list"?: string[];
|
||||||
|
"fail-list"?: string[];
|
||||||
|
"wifi-ap"?: Record<string, unknown>;
|
||||||
|
"wifi-ap-list"?: Record<string, unknown>[];
|
||||||
|
"main-cell-info"?: Record<string, unknown>;
|
||||||
|
"neighbor-cell-list"?: Record<string, unknown>[];
|
||||||
|
};
|
||||||
|
|
||||||
function parseBrokerUrl() {
|
function parseBrokerUrl() {
|
||||||
if (!env.MQTT_BROKER_URL) {
|
if (!env.MQTT_BROKER_URL) {
|
||||||
return { host: "", port: 0 };
|
return { host: "", port: 0 };
|
||||||
@ -48,6 +58,50 @@ function vendorError(res: Response, code: number, message: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDeviceDateTime(date: Date) {
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
return [
|
||||||
|
date.getUTCFullYear(),
|
||||||
|
pad(date.getUTCMonth() + 1),
|
||||||
|
pad(date.getUTCDate()),
|
||||||
|
pad(date.getUTCHours()),
|
||||||
|
pad(date.getUTCMinutes()),
|
||||||
|
pad(date.getUTCSeconds())
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberFromPayload(value: unknown, fallback: number) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppConfigVersion(payload: Qf100ConfigRequest) {
|
||||||
|
const requested = numberFromPayload(payload["app-config-version"], 1);
|
||||||
|
return Math.max(env.QF100_APP_CONFIG_VERSION || requested, requested);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultAudioConfig() {
|
||||||
|
return {
|
||||||
|
"voice-type": 2,
|
||||||
|
"voice-volume": 80,
|
||||||
|
"voice-format": 1,
|
||||||
|
"dialect-id": 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOptionalDynamicQrConfig(device: { capability_profile_json?: Record<string, unknown> }) {
|
||||||
|
const profile = device.capability_profile_json || {};
|
||||||
|
const url = String(profile.dynamic_qr_url || profile.dynamicQrUrl || env.QF100_DYNAMIC_QR_URL || "").trim();
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
gap: numberFromPayload(profile.dynamic_qr_gap || profile.dynamicQrGap, env.QF100_DYNAMIC_QR_GAP_SECONDS)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDevConfig(req: Request, res: Response, next: NextFunction) {
|
async function handleDevConfig(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const payload = getRequestPayload(req);
|
const payload = getRequestPayload(req);
|
||||||
@ -101,12 +155,13 @@ async function handleDevConfig(req: Request, res: Response, next: NextFunction)
|
|||||||
return vendorError(res, 1003, "mqtt broker is not configured");
|
return vendorError(res, 1003, "mqtt broker is not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dynamicQrConfig = buildOptionalDynamicQrConfig(device);
|
||||||
res.json({
|
res.json({
|
||||||
"error-code": 0,
|
"error-code": 0,
|
||||||
"bind-state": 1,
|
"bind-state": 1,
|
||||||
"app-config-version": 1,
|
"app-config-version": getAppConfigVersion(payload),
|
||||||
"time-stamp": Math.floor(now.getTime() / 1000),
|
"time-stamp": Math.floor(now.getTime() / 1000),
|
||||||
"date-time": now.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14),
|
"date-time": toDeviceDateTime(now),
|
||||||
mqtt: {
|
mqtt: {
|
||||||
"broker-ip": brokerHost,
|
"broker-ip": brokerHost,
|
||||||
"broker-port": brokerPort,
|
"broker-port": brokerPort,
|
||||||
@ -117,7 +172,98 @@ async function handleDevConfig(req: Request, res: Response, next: NextFunction)
|
|||||||
"publish-topic": `devices/${device.id}/uplink/qf100`,
|
"publish-topic": `devices/${device.id}/uplink/qf100`,
|
||||||
"keep-alive": env.QF100_MQTT_KEEP_ALIVE_SECONDS,
|
"keep-alive": env.QF100_MQTT_KEEP_ALIVE_SECONDS,
|
||||||
"cert-update": 0
|
"cert-update": 0
|
||||||
|
},
|
||||||
|
audio: buildDefaultAudioConfig(),
|
||||||
|
"trade-audio-templates": [],
|
||||||
|
"discount-audio-templates": [],
|
||||||
|
"verify-code-audio-templates": [],
|
||||||
|
"power-on-audio": {
|
||||||
|
"play-mode": 1,
|
||||||
|
"voice-format": 1,
|
||||||
|
name: "",
|
||||||
|
data: ""
|
||||||
|
},
|
||||||
|
"power-off-audio": {
|
||||||
|
"play-mode": 1,
|
||||||
|
"voice-format": 1,
|
||||||
|
name: "",
|
||||||
|
data: ""
|
||||||
|
},
|
||||||
|
"ad-audio": {
|
||||||
|
"play-mode": 0,
|
||||||
|
"time-period": {
|
||||||
|
start: 8,
|
||||||
|
end: 22
|
||||||
|
},
|
||||||
|
interval: 0,
|
||||||
|
"pay-count": 0,
|
||||||
|
"voice-format": 1,
|
||||||
|
name: "",
|
||||||
|
data: ""
|
||||||
|
},
|
||||||
|
"customer-audio": {
|
||||||
|
"play-mode": 0,
|
||||||
|
"voice-format": 1,
|
||||||
|
name: "",
|
||||||
|
data: ""
|
||||||
|
},
|
||||||
|
...(dynamicQrConfig ? { "dynamic-qr": dynamicQrConfig } : {})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordSpeakerReport(req: Request, state: string) {
|
||||||
|
const payload = getRequestPayload(req) as SpeakerReportRequest;
|
||||||
|
const serialNumber = String(payload["dev-sn"] || "").trim();
|
||||||
|
if (!serialNumber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = await getDeviceBySerialNumber(serialNumber);
|
||||||
|
if (!device) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
await patchDevice(device.id, {
|
||||||
|
last_seen_at: now.toISOString()
|
||||||
|
});
|
||||||
|
await createDeviceHeartbeat({
|
||||||
|
device_id: device.id,
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
firmware_version: payload["fw-version"] ? String(payload["fw-version"]) : device.firmware_version,
|
||||||
|
state,
|
||||||
|
payload_json: {
|
||||||
|
source: `speaker_${state}`,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAckReport(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
await recordSpeakerReport(req, req.path.replace(/^\//, "").replace(/-/g, "_"));
|
||||||
|
res.json({ "error-code": 0 });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCheckUpdate(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const payload = getRequestPayload(req) as SpeakerReportRequest;
|
||||||
|
await recordSpeakerReport(req, "check_update");
|
||||||
|
res.json({
|
||||||
|
"error-code": 1002,
|
||||||
|
version: payload["fw-version"] || "",
|
||||||
|
build: Number(payload["fw-build"] || 0),
|
||||||
|
"file-length": 0,
|
||||||
|
"file-hash": "",
|
||||||
|
"download-url": ""
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -126,5 +272,10 @@ async function handleDevConfig(req: Request, res: Response, next: NextFunction)
|
|||||||
|
|
||||||
router.get("/dev-config", handleDevConfig);
|
router.get("/dev-config", handleDevConfig);
|
||||||
router.post("/dev-config", handleDevConfig);
|
router.post("/dev-config", handleDevConfig);
|
||||||
|
router.post("/lbs-info", handleAckReport);
|
||||||
|
router.post("/config-update-result", handleAckReport);
|
||||||
|
router.get("/check-update", handleCheckUpdate);
|
||||||
|
router.post("/check-update", handleCheckUpdate);
|
||||||
|
router.post("/fw-update-result", handleAckReport);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user