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_PASSWORD=CHANGE_ME_MQTT_BACKEND_PASSWORD
|
||||
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_SWEEP_INTERVAL_MS=60000
|
||||
|
||||
@ -40,6 +40,9 @@ export const env = {
|
||||
QF100_MQTT_USERNAME: process.env.QF100_MQTT_USERNAME || "",
|
||||
QF100_MQTT_PASSWORD: process.env.QF100_MQTT_PASSWORD || "",
|
||||
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_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),
|
||||
|
||||
@ -18,6 +18,16 @@ type Qf100ConfigRequest = {
|
||||
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() {
|
||||
if (!env.MQTT_BROKER_URL) {
|
||||
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) {
|
||||
try {
|
||||
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");
|
||||
}
|
||||
|
||||
const dynamicQrConfig = buildOptionalDynamicQrConfig(device);
|
||||
res.json({
|
||||
"error-code": 0,
|
||||
"bind-state": 1,
|
||||
"app-config-version": 1,
|
||||
"app-config-version": getAppConfigVersion(payload),
|
||||
"time-stamp": Math.floor(now.getTime() / 1000),
|
||||
"date-time": now.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14),
|
||||
"date-time": toDeviceDateTime(now),
|
||||
mqtt: {
|
||||
"broker-ip": brokerHost,
|
||||
"broker-port": brokerPort,
|
||||
@ -117,7 +172,98 @@ async function handleDevConfig(req: Request, res: Response, next: NextFunction)
|
||||
"publish-topic": `devices/${device.id}/uplink/qf100`,
|
||||
"keep-alive": env.QF100_MQTT_KEEP_ALIVE_SECONDS,
|
||||
"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) {
|
||||
next(error);
|
||||
@ -126,5 +272,10 @@ async function handleDevConfig(req: Request, res: Response, next: NextFunction)
|
||||
|
||||
router.get("/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;
|
||||
|
||||
Reference in New Issue
Block a user