From 60c95e825df2e2589ad0f1a29e76154f3a9f1bf9 Mon Sep 17 00:00:00 2001 From: serversdwn Date: Wed, 24 Dec 2025 06:18:42 +0000 Subject: [PATCH] API built for most common commands --- API.md | 406 +++++++++++++++++++++++ app/__pycache__/routers.cpython-310.pyc | Bin 14246 -> 20495 bytes app/__pycache__/services.cpython-310.pyc | Bin 13446 -> 17676 bytes app/routers.py | 229 +++++++++++++ app/services.py | 193 +++++++++-- data/slmm.db | Bin 28672 -> 28672 bytes data/slmm.log | 146 ++++++++ templates/index.html | 236 ++++++++++++- 8 files changed, 1172 insertions(+), 38 deletions(-) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..ff0d276 --- /dev/null +++ b/API.md @@ -0,0 +1,406 @@ +# SLMM API Documentation + +REST API for controlling Rion NL-43/NL-53 Sound Level Meters via TCP and FTP. + +Base URL: `http://localhost:8000/api/nl43` + +All endpoints require a `unit_id` parameter identifying the device. + +## Device Configuration + +### Get Device Config +``` +GET /{unit_id}/config +``` +Returns the device configuration including host, port, and enabled protocols. + +**Response:** +```json +{ + "status": "ok", + "data": { + "unit_id": "nl43-1", + "host": "192.168.1.100", + "tcp_port": 2255, + "tcp_enabled": true, + "ftp_enabled": false, + "web_enabled": false + } +} +``` + +### Update Device Config +``` +PUT /{unit_id}/config +``` +Update device configuration. + +**Request Body:** +```json +{ + "host": "192.168.1.100", + "tcp_port": 2255, + "tcp_enabled": true, + "ftp_enabled": false, + "ftp_username": "admin", + "ftp_password": "password", + "web_enabled": false +} +``` + +## Device Status + +### Get Cached Status +``` +GET /{unit_id}/status +``` +Returns the last cached measurement snapshot from the database. + +### Get Live Status +``` +GET /{unit_id}/live +``` +Requests fresh DOD (Display On Demand) data from the device and returns current measurements. + +**Response:** +```json +{ + "status": "ok", + "data": { + "unit_id": "nl43-1", + "measurement_state": "Measure", + "lp": "65.2", + "leq": "68.4", + "lmax": "82.1", + "lmin": "42.3", + "lpeak": "89.5", + "battery_level": "80", + "power_source": "Battery", + "sd_remaining_mb": "2048", + "sd_free_ratio": "50" + } +} +``` + +### Stream Live Data (WebSocket) +``` +WS /{unit_id}/live +``` +Opens a WebSocket connection and streams continuous DRD (Display Real-time Data) from the device. + +## Measurement Control + +### Start Measurement +``` +POST /{unit_id}/start +``` +Starts measurement on the device. + +### Stop Measurement +``` +POST /{unit_id}/stop +``` +Stops measurement on the device. + +### Pause Measurement +``` +POST /{unit_id}/pause +``` +Pauses the current measurement. + +### Resume Measurement +``` +POST /{unit_id}/resume +``` +Resumes a paused measurement. + +### Reset Measurement +``` +POST /{unit_id}/reset +``` +Resets the measurement data. + +### Manual Store +``` +POST /{unit_id}/store +``` +Manually stores the current measurement data. + +## Device Information + +### Get Battery Level +``` +GET /{unit_id}/battery +``` +Returns the battery level. + +**Response:** +```json +{ + "status": "ok", + "battery_level": "80" +} +``` + +### Get Clock +``` +GET /{unit_id}/clock +``` +Returns the device clock time. + +**Response:** +```json +{ + "status": "ok", + "clock": "2025/12/24,02:30:15" +} +``` + +### Set Clock +``` +PUT /{unit_id}/clock +``` +Sets the device clock time. + +**Request Body:** +```json +{ + "datetime": "2025/12/24,02:30:15" +} +``` + +## Measurement Settings + +### Get Frequency Weighting +``` +GET /{unit_id}/frequency-weighting?channel=Main +``` +Gets the frequency weighting (A, C, or Z) for a channel. + +**Query Parameters:** +- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main) + +**Response:** +```json +{ + "status": "ok", + "frequency_weighting": "A", + "channel": "Main" +} +``` + +### Set Frequency Weighting +``` +PUT /{unit_id}/frequency-weighting +``` +Sets the frequency weighting. + +**Request Body:** +```json +{ + "weighting": "A", + "channel": "Main" +} +``` + +### Get Time Weighting +``` +GET /{unit_id}/time-weighting?channel=Main +``` +Gets the time weighting (F, S, or I) for a channel. + +**Query Parameters:** +- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main) + +**Response:** +```json +{ + "status": "ok", + "time_weighting": "F", + "channel": "Main" +} +``` + +### Set Time Weighting +``` +PUT /{unit_id}/time-weighting +``` +Sets the time weighting. + +**Request Body:** +```json +{ + "weighting": "F", + "channel": "Main" +} +``` + +**Values:** +- `F` - Fast (125ms) +- `S` - Slow (1s) +- `I` - Impulse (35ms) + +## FTP File Management + +### Enable FTP +``` +POST /{unit_id}/ftp/enable +``` +Enables FTP server on the device. + +**Note:** FTP and TCP are mutually exclusive. Enabling FTP will temporarily disable TCP control. + +### Disable FTP +``` +POST /{unit_id}/ftp/disable +``` +Disables FTP server on the device. + +### Get FTP Status +``` +GET /{unit_id}/ftp/status +``` +Checks if FTP is enabled on the device. + +**Response:** +```json +{ + "status": "ok", + "ftp_status": "On", + "ftp_enabled": true +} +``` + +### List Files +``` +GET /{unit_id}/ftp/files?path=/ +``` +Lists files and directories at the specified path. + +**Query Parameters:** +- `path` (optional): Directory path to list (default: /) + +**Response:** +```json +{ + "status": "ok", + "path": "/NL43_DATA/", + "count": 3, + "files": [ + { + "name": "measurement_001.wav", + "path": "/NL43_DATA/measurement_001.wav", + "size": 102400, + "modified": "Dec 24 2025", + "is_dir": false + }, + { + "name": "folder1", + "path": "/NL43_DATA/folder1", + "size": 0, + "modified": "Dec 23 2025", + "is_dir": true + } + ] +} +``` + +### Download File +``` +POST /{unit_id}/ftp/download +``` +Downloads a file from the device via FTP. + +**Request Body:** +```json +{ + "remote_path": "/NL43_DATA/measurement_001.wav" +} +``` + +**Response:** +Returns the file as a binary download with appropriate `Content-Disposition` header. + +## Error Responses + +All endpoints return standard HTTP status codes: + +- `200` - Success +- `404` - Device config not found +- `403` - TCP communication is disabled +- `502` - Failed to communicate with device +- `504` - Device communication timeout +- `500` - Internal server error + +**Error Response Format:** +```json +{ + "detail": "Error message" +} +``` + +## Common Patterns + +### Terra-view Integration Example + +```javascript +// Get live status from all devices +const devices = ['nl43-1', 'nl43-2', 'nl43-3']; +const statuses = await Promise.all( + devices.map(id => + fetch(`http://localhost:8000/api/nl43/${id}/live`) + .then(r => r.json()) + ) +); + +// Start measurement on all devices +await Promise.all( + devices.map(id => + fetch(`http://localhost:8000/api/nl43/${id}/start`, { method: 'POST' }) + ) +); + +// Download latest files from all devices +for (const device of devices) { + // Enable FTP + await fetch(`http://localhost:8000/api/nl43/${device}/ftp/enable`, { + method: 'POST' + }); + + // List files + const res = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/files?path=/NL43_DATA`); + const { files } = await res.json(); + + // Download latest file + const latestFile = files + .filter(f => !f.is_dir) + .sort((a, b) => b.modified - a.modified)[0]; + + if (latestFile) { + const download = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/download`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ remote_path: latestFile.path }) + }); + + const blob = await download.blob(); + // Process blob... + } + + // Disable FTP, re-enable TCP + await fetch(`http://localhost:8000/api/nl43/${device}/ftp/disable`, { + method: 'POST' + }); +} +``` + +## Rate Limiting + +The NL43 protocol requires ≥1 second between commands to the same device. The API automatically enforces this rate limit. + +## Notes + +- TCP and FTP protocols are mutually exclusive on the device +- FTP uses active mode (requires device to connect back to server) +- WebSocket streaming keeps a persistent connection - limit concurrent streams +- All measurements are stored in the database for quick access via `/status` endpoint diff --git a/app/__pycache__/routers.cpython-310.pyc b/app/__pycache__/routers.cpython-310.pyc index 97e9712a8e10d827836fbcbb0150249e6dc3d14d..c455f0e54e340754b0288369d725e6f5c61e3dc8 100644 GIT binary patch delta 4741 zcmb7HTX0jy89sY-B+IsLma#0~#KEbJg>2adjLps1fdDR-m|JO`ixJub$jB0`j-gg% z%&kp_6q2why%1WvFm0K%X{b60&5#)~(CPFclfLvt6Q=1yANtVgWO(aC+yCDq$wx+H z(qy!(|NSrf{kQ$k&y%ms5m(abtdiir-_GoaRZhL(YL?$o?sPsPmUg>Cpsl65!T9rJ7s6Y(X!C5=q zD}1|;?ic*)1?_;KeT^Ozv<*V<*M+~oLB|AbqoBP2dS5K@(@RA^y-ddi=OB3a5RHI` zBf`U*goVRTfZUXKTa;2EH$-jwq@eZ>OsAkVS2;X7vNxISu>7mu#9>P|>_b*-1QD zx-}AuYgE;f2HC7hO(s+}0bJ}jEPN=m&Sn*|{5zqphFp!1A^IMfT=Zu5ef@v)CDvK z-K^|MI2AU73-PSZd5u5JfEoZ0y8!oO-$o$j7y`4V^FT+#GX)?`dyyoUe}6>oYmOB9PpVrhB0>okh^uyIEah*NIrvKMdRkc zN8`!par8_J`!1XXU1=X;pxUWe6bcegRXsMz)nNNtGmM;7oTdE>If_6GfpP$sTN(KR%$b% zr`v#50Y5wZD&bc}D=sTDO1d_q%vEQ|gkzoSB&Kr(Y=K2tU`^ z7=}hyE;-z3-@d0fcB4@%PJv7nRhLwa5;3_K>E+yuoosz2oEpv*E@7tuI@^iM6fV86 z+_tebxZjRTKQ3EwnZ`JDao8RQ=cNL$k>z$wCSJ0{oI@O<)H2%&jOBtf zW)%UmK)@6Tm^J4)sKt&R(_@LF`Jm-0PZuaTWmhDY$W=s-MG^@uzB_1RVZ2l~EcdDB^)v+MauqvLj}=?wHfb%FT@^-li71(}3q5@eQmi?A0c*2ddU+tZOfBjp01P?>JQ5HGS@f?|&mUElsvr1`+IpQ2t7R}&U-Sjun0f+1by#gHz7@g<_^z6JK~ zA)t9Qd@Ebq6t|F1_}Reb`d6x}iBgpLLs>xPWI3Z6a1 z^x(OQ4_*y$x}mFKZ*%6W#V8gRb(fAuFid%-lSIP*g_LJOKlH*H>NWV4SxK*%el=f? zhJo5L2A0seQUmZU;Y$ySz8OnI;%YP!k50uSdJM;YfiR_ZY}vUv2&I&q6mxAnk}CPu z2(tlr654~yW|!~PbQXj8jh4)$1w9z0g&~&>p+_4_bJWbb@YTWM$l8oalAO|2$WeS3 zi}w@JpOmCdi6xM+?Lb2N<_RsT!#y;{z-k!@zNhgy{@<1=e>`fu_`5)q>9~lu{}f*r zSZc4xUx>q?=)~H{|NS}PpEd^g%|M+3A2@Gk9|msQ`hJ1JX;^Msj7dQZ4mdd*5AZVB z<(w%kLB~^x$cfakq~0lKf7aGUlwZRU%&T?eH~eqyqeDVQNaI0%;=tVr6V9$-36Y*a^aiI?lZp0?(#h=`pEm)S5|Zp_Xogo z3Ru|hQQrsI2ceB})rYXZ1jLk>hxyX5lYE%%3cpXteg60Eztr7F-bGK=M>rMP9>B>B zng6qA1$ls@slr|Zbh~?7W%dUDv)%802n@eNdsk7%!)&(qk_=i0SKn&550sq!a0I** znbg=H@K0qT8B6d(p+;WUcYyqnAMXp=K8Az4GXHVkkk|%?`DcAIwm$)tWqEYXwTbh% z#Sb=yA2tjpc!p!M7;hQJ9S%JV-%U1$3-&LD9e`o2v$t^(fp`yh=thQ)V(1u#7#ZSV x9|NR%zP8^@F7c86ExonP#9sJ$n~BG3S5#cBt;FtC;aB(<_?E%1kzeWe{|~w1C6WLD delta 742 zcmYLG-Aj{E9DaZAwwKMETe`U=Go57f)y=u7vyXi_Yxa_>k?dw&)asm(;TpWF$Up)& z=?nbW!B8}s&;lulcrWU2=%Ownh(UiqCe&q@opTs<;50*E;RFi=h<+unHNKGo;Ql5SwWus!Pb-w4bx;=xm45+ z^uHY1AO8&<7)Wk(3d-6+Xisu1QImR~!k$i@U_8X2Lf%AN;xuR8BuZNdkZD%ID4=C89NRqf|gL^NzrUMHVCPI5MBfekH_d=Icm%;&z^H<^tO z|9_k<66kkI+cfe4h+P?kZ4y^pU8PQ{U?B@g$ch;O>*(J?SU}<|xvKPuvVq(j=pycR zTgMJHex^f|^pvy+t)T8`uazev?HQVRX=Obd#%s9}zLhIr^*Py}xd$)E^^`2tY3K`( zYAVjDJVw5y7NuRPs!z$~3(w>-qx>OP7-P?%vYl17qsnGhS$DNgy4xMrhA~_yqC4|A fN~aM@S+aET%n`Q@j2RagJDRt>53HR={W16pxM;s| diff --git a/app/__pycache__/services.cpython-310.pyc b/app/__pycache__/services.cpython-310.pyc index 3c8764f1fab1f58c9a46e566965458d14e7c666a..acd25a246b990492aaec3b813758000b971e6524 100644 GIT binary patch delta 7517 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�dc(Zi@)=+f`ZLrMorrcM? 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 diff --git a/app/routers.py b/app/routers.py index 3e5dff5..bf030f7 100644 --- a/app/routers.py +++ b/app/routers.py @@ -258,6 +258,208 @@ async def manual_store(unit_id: str, db: Session = Depends(get_db)): raise HTTPException(status_code=500, detail="Internal server error") +@router.post("/{unit_id}/pause") +async def pause_measurement(unit_id: str, db: Session = Depends(get_db)): + """Pause the current measurement.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.pause() + logger.info(f"Paused measurement on unit {unit_id}") + return {"status": "ok", "message": "Measurement paused"} + except Exception as e: + logger.error(f"Failed to pause measurement on {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.post("/{unit_id}/resume") +async def resume_measurement(unit_id: str, db: Session = Depends(get_db)): + """Resume a paused measurement.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.resume() + logger.info(f"Resumed measurement on unit {unit_id}") + return {"status": "ok", "message": "Measurement resumed"} + except Exception as e: + logger.error(f"Failed to resume measurement on {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.post("/{unit_id}/reset") +async def reset_measurement(unit_id: str, db: Session = Depends(get_db)): + """Reset the measurement data.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.reset() + logger.info(f"Reset measurement data on unit {unit_id}") + return {"status": "ok", "message": "Measurement data reset"} + except Exception as e: + logger.error(f"Failed to reset measurement on {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.get("/{unit_id}/battery") +async def get_battery(unit_id: str, db: Session = Depends(get_db)): + """Get battery level.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + level = await client.get_battery_level() + return {"status": "ok", "battery_level": level} + except Exception as e: + logger.error(f"Failed to get battery level for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.get("/{unit_id}/clock") +async def get_clock(unit_id: str, db: Session = Depends(get_db)): + """Get device clock time.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + clock = await client.get_clock() + return {"status": "ok", "clock": clock} + except Exception as e: + logger.error(f"Failed to get clock for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +class ClockPayload(BaseModel): + datetime: str # Format: YYYY/MM/DD,HH:MM:SS + + +@router.put("/{unit_id}/clock") +async def set_clock(unit_id: str, payload: ClockPayload, db: Session = Depends(get_db)): + """Set device clock time.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.set_clock(payload.datetime) + return {"status": "ok", "message": f"Clock set to {payload.datetime}"} + except Exception as e: + logger.error(f"Failed to set clock for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +class WeightingPayload(BaseModel): + weighting: str + channel: str = "Main" + + +@router.get("/{unit_id}/frequency-weighting") +async def get_frequency_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)): + """Get frequency weighting (A, C, Z).""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + weighting = await client.get_frequency_weighting(channel) + return {"status": "ok", "frequency_weighting": weighting, "channel": channel} + except Exception as e: + logger.error(f"Failed to get frequency weighting for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.put("/{unit_id}/frequency-weighting") +async def set_frequency_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)): + """Set frequency weighting (A, C, Z).""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.set_frequency_weighting(payload.weighting, payload.channel) + return {"status": "ok", "message": f"Frequency weighting set to {payload.weighting} on {payload.channel}"} + except Exception as e: + logger.error(f"Failed to set frequency weighting for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.get("/{unit_id}/time-weighting") +async def get_time_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)): + """Get time weighting (F, S, I).""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + weighting = await client.get_time_weighting(channel) + return {"status": "ok", "time_weighting": weighting, "channel": channel} + except Exception as e: + logger.error(f"Failed to get time weighting for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + +@router.put("/{unit_id}/time-weighting") +async def set_time_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)): + """Set time weighting (F, S, I).""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + await client.set_time_weighting(payload.weighting, payload.channel) + return {"status": "ok", "message": f"Time weighting set to {payload.weighting} on {payload.channel}"} + except Exception as e: + logger.error(f"Failed to set time weighting for {unit_id}: {e}") + raise HTTPException(status_code=502, detail=str(e)) + + @router.get("/{unit_id}/live") async def live_status(unit_id: str, db: Session = Depends(get_db)): cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() @@ -292,6 +494,33 @@ async def live_status(unit_id: str, db: Session = Depends(get_db)): raise HTTPException(status_code=500, detail="Internal server error") +@router.get("/{unit_id}/results") +async def get_results(unit_id: str, db: Session = Depends(get_db)): + """Get final calculation results (DLC) from the last measurement.""" + cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first() + if not cfg: + raise HTTPException(status_code=404, detail="NL43 config not found") + + if not cfg.tcp_enabled: + raise HTTPException(status_code=403, detail="TCP communication is disabled for this device") + + client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password) + try: + results = await client.request_dlc() + logger.info(f"Retrieved measurement results for unit {unit_id}") + return {"status": "ok", "data": results} + + except ConnectionError as e: + logger.error(f"Failed to get results for {unit_id}: {e}") + raise HTTPException(status_code=502, detail="Failed to communicate with device") + except TimeoutError: + logger.error(f"Timeout getting results for {unit_id}") + raise HTTPException(status_code=504, detail="Device communication timeout") + except Exception as e: + logger.error(f"Unexpected error getting results for {unit_id}: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + + @router.websocket("/{unit_id}/stream") async def stream_live(websocket: WebSocket, unit_id: str): """WebSocket endpoint for real-time DRD streaming from NL43 device. diff --git a/app/services.py b/app/services.py index 3462a2f..5558151 100644 --- a/app/services.py +++ b/app/services.py @@ -13,7 +13,8 @@ from dataclasses import dataclass from datetime import datetime from typing import Optional, List from sqlalchemy.orm import Session -import aioftp +from ftplib import FTP +from pathlib import Path from app.models import NL43Status @@ -237,6 +238,102 @@ class NL43Client: await self._send_command("Manual Store,Start\r\n") logger.info(f"Manual store executed on {self.device_key}") + async def pause(self): + """Pause the current measurement.""" + await self._send_command("Pause,On\r\n") + logger.info(f"Measurement paused on {self.device_key}") + + async def resume(self): + """Resume a paused measurement.""" + await self._send_command("Pause,Off\r\n") + logger.info(f"Measurement resumed on {self.device_key}") + + async def reset(self): + """Reset the measurement data.""" + await self._send_command("Reset\r\n") + logger.info(f"Measurement data reset on {self.device_key}") + + async def get_battery_level(self) -> str: + """Get the battery level.""" + resp = await self._send_command("Battery Level?\r\n") + logger.info(f"Battery level on {self.device_key}: {resp}") + return resp.strip() + + async def get_clock(self) -> str: + """Get the device clock time.""" + resp = await self._send_command("Clock?\r\n") + logger.info(f"Clock on {self.device_key}: {resp}") + return resp.strip() + + async def set_clock(self, datetime_str: str): + """Set the device clock time. + + Args: + datetime_str: Time in format YYYY/MM/DD,HH:MM:SS + """ + await self._send_command(f"Clock,{datetime_str}\r\n") + logger.info(f"Clock set on {self.device_key} to {datetime_str}") + + async def get_frequency_weighting(self, channel: str = "Main") -> str: + """Get frequency weighting (A, C, Z, etc.). + + Args: + channel: Main, Sub1, Sub2, or Sub3 + """ + resp = await self._send_command(f"Frequency Weighting ({channel})?\r\n") + logger.info(f"Frequency weighting ({channel}) on {self.device_key}: {resp}") + return resp.strip() + + async def set_frequency_weighting(self, weighting: str, channel: str = "Main"): + """Set frequency weighting. + + Args: + weighting: A, C, or Z + channel: Main, Sub1, Sub2, or Sub3 + """ + await self._send_command(f"Frequency Weighting ({channel}),{weighting}\r\n") + logger.info(f"Frequency weighting ({channel}) set to {weighting} on {self.device_key}") + + async def get_time_weighting(self, channel: str = "Main") -> str: + """Get time weighting (F, S, I). + + Args: + channel: Main, Sub1, Sub2, or Sub3 + """ + resp = await self._send_command(f"Time Weighting ({channel})?\r\n") + logger.info(f"Time weighting ({channel}) on {self.device_key}: {resp}") + return resp.strip() + + async def set_time_weighting(self, weighting: str, channel: str = "Main"): + """Set time weighting. + + Args: + weighting: F (Fast), S (Slow), or I (Impulse) + channel: Main, Sub1, Sub2, or Sub3 + """ + await self._send_command(f"Time Weighting ({channel}),{weighting}\r\n") + logger.info(f"Time weighting ({channel}) set to {weighting} on {self.device_key}") + + async def request_dlc(self) -> dict: + """Request DLC (Data Last Calculation) - final stored measurement results. + + This retrieves the complete calculation results from the last/current measurement, + including all statistical data. Similar to DOD but for final results. + + Returns: + Dict with parsed DLC data + """ + resp = await self._send_command("DLC?\r\n") + logger.info(f"DLC data received from {self.device_key}: {resp[:100]}...") + + # Parse DLC response - similar format to DOD + # The exact format depends on device configuration + # For now, return raw data - can be enhanced based on actual response format + return { + "raw_data": resp.strip(), + "device_key": self.device_key, + } + async def stream_drd(self, callback): """Stream continuous DRD output from the device. @@ -380,23 +477,45 @@ class NL43Client: """ logger.info(f"Listing FTP files on {self.device_key} at {remote_path}") - try: - # FTP uses standard port 21, not the TCP control port - async with aioftp.Client.context( - self.host, - port=21, - user=self.ftp_username, - password=self.ftp_password, - socket_timeout=10 - ) as client: + def _list_ftp_sync(): + """Synchronous FTP listing using ftplib (supports active mode).""" + ftp = FTP() + ftp.set_debuglevel(0) + try: + # Connect and login + ftp.connect(self.host, 21, timeout=10) + ftp.login(self.ftp_username, self.ftp_password) + ftp.set_pasv(False) # Force active mode + + # Change to target directory + if remote_path != "/": + ftp.cwd(remote_path) + + # Get directory listing with details files = [] - async for path, info in client.list(remote_path): + lines = [] + ftp.retrlines('LIST', lines.append) + + for line in lines: + # Parse Unix-style ls output + parts = line.split(None, 8) + if len(parts) < 9: + continue + + is_dir = parts[0].startswith('d') + size = int(parts[4]) if not is_dir else 0 + name = parts[8] + + # Skip . and .. + if name in ('.', '..'): + continue + file_info = { - "name": path.name, - "path": str(path), - "size": info.get("size", 0), - "modified": info.get("modify", ""), - "is_dir": info["type"] == "dir", + "name": name, + "path": f"{remote_path.rstrip('/')}/{name}", + "size": size, + "modified": f"{parts[5]} {parts[6]} {parts[7]}", + "is_dir": is_dir, } files.append(file_info) logger.debug(f"Found file: {file_info}") @@ -404,6 +523,15 @@ class NL43Client: logger.info(f"Found {len(files)} files/directories on {self.device_key}") return files + finally: + try: + ftp.quit() + except: + pass + + try: + # Run synchronous FTP in thread pool + return await asyncio.to_thread(_list_ftp_sync) except Exception as e: logger.error(f"Failed to list FTP files on {self.device_key}: {e}") raise ConnectionError(f"FTP connection failed: {str(e)}") @@ -417,18 +545,31 @@ class NL43Client: """ logger.info(f"Downloading {remote_path} from {self.device_key} to {local_path}") - try: - # FTP uses standard port 21, not the TCP control port - async with aioftp.Client.context( - self.host, - port=21, - user=self.ftp_username, - password=self.ftp_password, - socket_timeout=10 - ) as client: - await client.download(remote_path, local_path, write_into=True) + def _download_ftp_sync(): + """Synchronous FTP download using ftplib (supports active mode).""" + ftp = FTP() + ftp.set_debuglevel(0) + try: + # Connect and login + ftp.connect(self.host, 21, timeout=10) + ftp.login(self.ftp_username, self.ftp_password) + ftp.set_pasv(False) # Force active mode + + # Download file + with open(local_path, 'wb') as f: + ftp.retrbinary(f'RETR {remote_path}', f.write) + logger.info(f"Successfully downloaded {remote_path} to {local_path}") + finally: + try: + ftp.quit() + except: + pass + + try: + # Run synchronous FTP in thread pool + await asyncio.to_thread(_download_ftp_sync) except Exception as e: logger.error(f"Failed to download {remote_path} from {self.device_key}: {e}") raise ConnectionError(f"FTP download failed: {str(e)}") diff --git a/data/slmm.db b/data/slmm.db index 24c7a61c964981b7588c1c2e094bed2b6c53aa10..c8a4bfd794f1e4e8457063141e769c9dbf325477 100644 GIT binary patch delta 347 zcmZp8z}WDBae_2s|3n#Q#{P{7%lKJtGVtHrEGTe_Uq_z-2viw4B~_U?IXFZ`^Kwj# zbq&po^-N6l49yJnj14%>hX%U_834g11y%)CzEB3fP@rCKzWQbjMt(_0X;BaW8PnUo0?c$T9j&Hre|bgtY>awqGt%CO-wEI z3_xN!3LsG(1ye8yk_C&H07(ghtr$#wqvlh6CJ S;lX-%#qUAoB#SVD*ym;!!h&# diff --git a/data/slmm.log b/data/slmm.log index b365629..3bec438 100644 --- a/data/slmm.log +++ b/data/slmm.log @@ -379,3 +379,149 @@ 2025-12-24 02:02:08,074 - app.main - INFO - CORS allowed origins: ['*'] 2025-12-24 02:02:13,115 - app.main - INFO - Database tables initialized 2025-12-24 02:02:13,115 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:03:20,909 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:03:21,218 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail'] +2025-12-24 02:03:21,218 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail'] +2025-12-24 02:03:26,148 - app.main - INFO - Database tables initialized +2025-12-24 02:03:26,149 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:04:55,026 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:04:55,339 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail'] +2025-12-24 02:04:55,339 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail'] +2025-12-24 02:12:30,900 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:12:31,978 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('227',) but got 502 [' Not Implemented'] +2025-12-24 02:12:31,978 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('227',) but got 502 [' Not Implemented'] +2025-12-24 02:12:42,647 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On +2025-12-24 02:12:42,819 - app.services - INFO - FTP enabled on 63.45.161.30:2255 +2025-12-24 02:12:42,819 - app.routers - INFO - Enabled FTP on unit nl43-1 +2025-12-24 02:12:46,890 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:12:47,779 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('227',) but got 502 [' Not Implemented'] +2025-12-24 02:12:47,779 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('227',) but got 502 [' Not Implemented'] +2025-12-24 02:14:28,289 - app.main - INFO - Database tables initialized +2025-12-24 02:14:28,289 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:14:29,306 - app.main - INFO - Database tables initialized +2025-12-24 02:14:29,306 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:14:58,933 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On +2025-12-24 02:14:59,099 - app.services - INFO - FTP enabled on 63.45.161.30:2255 +2025-12-24 02:14:59,099 - app.routers - INFO - Enabled FTP on unit nl43-1 +2025-12-24 02:14:59,921 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:15:00,339 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: No passive commands provided +2025-12-24 02:15:00,339 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: No passive commands provided +2025-12-24 02:15:32,844 - app.main - INFO - Database tables initialized +2025-12-24 02:15:32,844 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:15:34,474 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:15:34,859 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: No passive commands provided +2025-12-24 02:15:34,859 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: No passive commands provided +2025-12-24 02:16:31,671 - app.main - INFO - Database tables initialized +2025-12-24 02:16:31,671 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:17:13,560 - app.main - INFO - Database tables initialized +2025-12-24 02:17:13,560 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:17:29,976 - app.main - INFO - Database tables initialized +2025-12-24 02:17:29,976 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:30:23,927 - app.main - INFO - Database tables initialized +2025-12-24 02:30:23,928 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:30:43,933 - app.routers - INFO - WebSocket connection accepted for unit nl43-1 +2025-12-24 02:30:43,934 - app.routers - INFO - Starting DRD stream for unit nl43-1 +2025-12-24 02:30:43,934 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255 +2025-12-24 02:30:44,099 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255 +2025-12-24 02:30:47,915 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal]) +2025-12-24 02:30:47,916 - app.services - INFO - DRD stream ended for 63.45.161.30:2255 +2025-12-24 02:30:47,916 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal]) +2025-12-24 02:30:47,916 - app.routers - INFO - WebSocket stream closed for unit nl43-1 +2025-12-24 02:30:50,949 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD? +2025-12-24 02:30:51,149 - app.services - INFO - Parsed 64 data points from DOD response +2025-12-24 02:30:51,159 - app.routers - INFO - Retrieved live status for unit nl43-1 +2025-12-24 02:30:54,330 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:30:55,059 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:31:12,298 - app.services - INFO - Downloading /NIKON001.DSC from 63.45.161.30:2255 to data/downloads/nl43-1/NIKON001.DSC +2025-12-24 02:31:13,140 - app.services - INFO - Successfully downloaded /NIKON001.DSC to data/downloads/nl43-1/NIKON001.DSC +2025-12-24 02:31:13,220 - app.routers - INFO - Downloaded /NIKON001.DSC from nl43-1 to data/downloads/nl43-1/NIKON001.DSC +2025-12-24 02:34:49,457 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:34:50,419 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:34:52,824 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43 +2025-12-24 02:34:53,709 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255 +2025-12-24 02:35:00,136 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Screenshot +2025-12-24 02:35:00,942 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255 +2025-12-24 02:35:03,332 - app.services - INFO - Downloading /NL-43/Screenshot/0001_20251223_193950.bmp from 63.45.161.30:2255 to data/downloads/nl43-1/0001_20251223_193950.bmp +2025-12-24 02:35:04,939 - app.services - INFO - Successfully downloaded /NL-43/Screenshot/0001_20251223_193950.bmp to data/downloads/nl43-1/0001_20251223_193950.bmp +2025-12-24 02:35:05,019 - app.routers - INFO - Downloaded /NL-43/Screenshot/0001_20251223_193950.bmp from nl43-1 to data/downloads/nl43-1/0001_20251223_193950.bmp +2025-12-24 02:35:20,669 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/ +2025-12-24 02:35:21,539 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255 +2025-12-24 02:35:24,452 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019 +2025-12-24 02:35:25,339 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255 +2025-12-24 02:35:30,134 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/Auto_Lp_01 +2025-12-24 02:35:30,939 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255 +2025-12-24 02:35:34,225 - app.services - INFO - Downloading /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:35:36,139 - app.services - INFO - Successfully downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:35:36,219 - app.routers - INFO - Downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:36:30,080 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:36:30,779 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:36:34,289 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43 +2025-12-24 02:36:35,109 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255 +2025-12-24 02:36:40,284 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019 +2025-12-24 02:36:41,220 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255 +2025-12-24 02:36:43,370 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/Auto_Lp_01 +2025-12-24 02:36:44,428 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255 +2025-12-24 02:36:46,101 - app.services - INFO - Downloading /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:36:47,380 - app.services - INFO - Successfully downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:36:47,476 - app.routers - INFO - Downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd +2025-12-24 02:37:18,786 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/ +2025-12-24 02:37:19,580 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255 +2025-12-24 02:37:25,812 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/ +2025-12-24 02:37:26,619 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255 +2025-12-24 02:37:28,988 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Manual_0019 +2025-12-24 02:37:29,859 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255 +2025-12-24 02:37:31,775 - app.services - INFO - Downloading /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd +2025-12-24 02:37:32,659 - app.services - INFO - Successfully downloaded /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd +2025-12-24 02:37:32,739 - app.routers - INFO - Downloaded /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd +2025-12-24 02:38:02,603 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:38:03,379 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:38:19,338 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On +2025-12-24 02:38:19,499 - app.services - INFO - FTP enabled on 63.45.161.30:2255 +2025-12-24 02:38:19,499 - app.routers - INFO - Enabled FTP on unit nl43-1 +2025-12-24 02:38:20,339 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On +2025-12-24 02:38:20,500 - app.services - INFO - FTP enabled on 63.45.161.30:2255 +2025-12-24 02:38:20,500 - app.routers - INFO - Enabled FTP on unit nl43-1 +2025-12-24 02:38:23,612 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:38:24,339 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:38:31,856 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/ +2025-12-24 02:38:32,660 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255 +2025-12-24 02:38:35,781 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:38:36,500 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:38:39,724 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /DCIM +2025-12-24 02:38:40,499 - app.services - INFO - Found 0 files/directories on 63.45.161.30:2255 +2025-12-24 02:38:45,065 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 02:38:45,939 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 02:41:25,498 - app.main - INFO - Database tables initialized +2025-12-24 02:41:25,498 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 02:42:08,371 - app.main - INFO - Database tables initialized +2025-12-24 02:42:08,372 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 05:44:06,113 - app.main - INFO - Database tables initialized +2025-12-24 05:44:06,113 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 05:44:19,375 - app.main - INFO - Database tables initialized +2025-12-24 05:44:19,376 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 06:11:27,237 - app.main - INFO - Database tables initialized +2025-12-24 06:11:27,237 - app.main - INFO - CORS allowed origins: ['*'] +2025-12-24 06:11:31,752 - app.services - INFO - Sending command to 63.45.161.30:2255: Battery Level? +2025-12-24 06:11:31,901 - app.services - INFO - Battery level on 63.45.161.30:2255: Full +2025-12-24 06:11:34,598 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock? +2025-12-24 06:11:34,742 - app.services - INFO - Clock on 63.45.161.30:2255: 2025/12/24 02:10:55 +2025-12-24 06:11:39,102 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:11:41 +2025-12-24 06:11:39,262 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value +2025-12-24 06:11:39,262 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value +2025-12-24 06:12:00,090 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at / +2025-12-24 06:12:00,789 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255 +2025-12-24 06:12:12,263 - app.services - INFO - Sending command to 63.45.161.30:2255: Frequency Weighting (Main)? +2025-12-24 06:12:12,432 - app.services - INFO - Frequency weighting (Main) on 63.45.161.30:2255: A +2025-12-24 06:12:23,787 - app.services - INFO - Sending command to 63.45.161.30:2255: Time Weighting (Main)? +2025-12-24 06:12:23,942 - app.services - INFO - Time weighting (Main) on 63.45.161.30:2255: S +2025-12-24 06:12:41,070 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:12:43 +2025-12-24 06:12:41,232 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value +2025-12-24 06:12:41,232 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value +2025-12-24 06:13:04,056 - app.services - INFO - Sending command to 63.45.161.30:2255: DLC? +2025-12-24 06:13:04,232 - app.services - INFO - DLC data received from 63.45.161.30:2255: -.-, 43.7, 53.7, 44.1, 43.4, 44.2, 44.2, 43.7, 43.4, 43.3, 59.0, -.-, -.-, -.-,0,0, -.-, -.-, ... +2025-12-24 06:13:04,232 - app.routers - INFO - Retrieved measurement results for unit nl43-1 +2025-12-24 06:13:29,235 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start +2025-12-24 06:13:29,422 - app.routers - INFO - Started measurement on unit nl43-1 +2025-12-24 06:13:55,260 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:13:57 +2025-12-24 06:13:55,422 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value +2025-12-24 06:13:55,422 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value diff --git a/templates/index.html b/templates/index.html index 3b5a088..e1090b1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -30,11 +30,40 @@
- Controls + Measurement Controls + + + - + + +
+ +
+ Device Info + + + +
+ +
+ Measurement Settings +
+ + + + + +
+
+ + + + + +
@@ -134,6 +163,134 @@ log(`Live: ${JSON.stringify(data.data)}`); } + async function getResults() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/results`); + const data = await res.json(); + if (!res.ok) { + log(`Get Results failed: ${res.status} ${JSON.stringify(data)}`); + return; + } + statusEl.textContent = JSON.stringify(data.data, null, 2); + log(`Results (DLC): Retrieved final calculation data`); + } + + // New measurement control functions + async function pause() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/pause`, { method: 'POST' }); + const data = await res.json(); + log(`Pause: ${res.status} - ${data.message || JSON.stringify(data)}`); + } + + async function resume() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/resume`, { method: 'POST' }); + const data = await res.json(); + log(`Resume: ${res.status} - ${data.message || JSON.stringify(data)}`); + } + + async function reset() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/reset`, { method: 'POST' }); + const data = await res.json(); + log(`Reset: ${res.status} - ${data.message || JSON.stringify(data)}`); + } + + // Device info functions + async function getBattery() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/battery`); + const data = await res.json(); + if (res.ok) { + log(`Battery Level: ${data.battery_level}%`); + } else { + log(`Get Battery failed: ${res.status} - ${data.detail}`); + } + } + + async function getClock() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/clock`); + const data = await res.json(); + if (res.ok) { + log(`Device Clock: ${data.clock}`); + } else { + log(`Get Clock failed: ${res.status} - ${data.detail}`); + } + } + + async function syncClock() { + const unitId = document.getElementById('unitId').value; + const now = new Date(); + const datetime = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')},${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; + + const res = await fetch(`/api/nl43/${unitId}/clock`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ datetime }) + }); + const data = await res.json(); + if (res.ok) { + log(`Clock synced to: ${datetime}`); + } else { + log(`Sync Clock failed: ${res.status} - ${data.detail}`); + } + } + + // Measurement settings functions + async function getFreqWeighting() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`); + const data = await res.json(); + if (res.ok) { + log(`Frequency Weighting (Main): ${data.frequency_weighting}`); + } else { + log(`Get Freq Weighting failed: ${res.status} - ${data.detail}`); + } + } + + async function setFreqWeighting(weighting) { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/frequency-weighting`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ weighting, channel: 'Main' }) + }); + const data = await res.json(); + if (res.ok) { + log(`Frequency Weighting set to: ${weighting}`); + } else { + log(`Set Freq Weighting failed: ${res.status} - ${data.detail}`); + } + } + + async function getTimeWeighting() { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/time-weighting?channel=Main`); + const data = await res.json(); + if (res.ok) { + log(`Time Weighting (Main): ${data.time_weighting}`); + } else { + log(`Get Time Weighting failed: ${res.status} - ${data.detail}`); + } + } + + async function setTimeWeighting(weighting) { + const unitId = document.getElementById('unitId').value; + const res = await fetch(`/api/nl43/${unitId}/time-weighting`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ weighting, channel: 'Main' }) + }); + const data = await res.json(); + if (res.ok) { + log(`Time Weighting set to: ${weighting}`); + } else { + log(`Set Time Weighting failed: ${res.status} - ${data.detail}`); + } + } + function toggleStream() { if (ws && ws.readyState === WebSocket.OPEN) { stopStream(); @@ -239,9 +396,12 @@ } } - async function listFiles() { + let currentPath = '/'; + + async function listFiles(path = '/') { + currentPath = path; const unitId = document.getElementById('unitId').value; - const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=/`); + const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=${encodeURIComponent(path)}`); const data = await res.json(); if (!res.ok) { @@ -252,13 +412,58 @@ const fileListEl = document.getElementById('fileList'); fileListEl.innerHTML = ''; + // Add breadcrumb navigation + const breadcrumb = document.createElement('div'); + breadcrumb.style.marginBottom = '8px'; + breadcrumb.style.padding = '4px'; + breadcrumb.style.background = '#e1e4e8'; + breadcrumb.style.borderRadius = '3px'; + breadcrumb.innerHTML = 'Path: '; + + const pathParts = path.split('/').filter(p => p); + let builtPath = '/'; + + // Root link + const rootLink = document.createElement('a'); + rootLink.href = '#'; + rootLink.textContent = '/'; + rootLink.style.marginRight = '4px'; + rootLink.onclick = (e) => { e.preventDefault(); listFiles('/'); }; + breadcrumb.appendChild(rootLink); + + // Path component links + pathParts.forEach((part, idx) => { + builtPath += part + '/'; + const linkPath = builtPath; + + const separator = document.createElement('span'); + separator.textContent = ' / '; + breadcrumb.appendChild(separator); + + const link = document.createElement('a'); + link.href = '#'; + link.textContent = part; + link.style.marginRight = '4px'; + if (idx === pathParts.length - 1) { + link.style.fontWeight = 'bold'; + link.style.color = '#000'; + } + link.onclick = (e) => { e.preventDefault(); listFiles(linkPath); }; + breadcrumb.appendChild(link); + }); + + fileListEl.appendChild(breadcrumb); + if (data.files.length === 0) { - fileListEl.textContent = 'No files found'; - log(`No files found on device`); + const emptyDiv = document.createElement('div'); + emptyDiv.textContent = 'No files found'; + emptyDiv.style.padding = '8px'; + fileListEl.appendChild(emptyDiv); + log(`No files found in ${path}`); return; } - log(`Found ${data.count} files on device`); + log(`Found ${data.count} files in ${path}`); data.files.forEach(file => { const fileDiv = document.createElement('div'); @@ -269,11 +474,18 @@ const icon = file.is_dir ? '📁' : '📄'; const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`; - fileDiv.innerHTML = ` - ${icon} ${file.name}${size} - ${!file.is_dir ? `` : ''} -
${file.path} - `; + if (file.is_dir) { + fileDiv.innerHTML = ` + ${icon} ${file.name} +
${file.path} + `; + } else { + fileDiv.innerHTML = ` + ${icon} ${file.name}${size} + +
${file.path} + `; + } fileListEl.appendChild(fileDiv); });