Checkpoint EEPROM, firmware analysis and instrument protocol updates

This commit is contained in:
2026-10-08 21:51:44 +03:00
parent b0c0c05cfa
commit fe9b5a246e
24 changed files with 848 additions and 62 deletions

View File

@@ -0,0 +1,44 @@
/* Private PM35 FC03 EEPROM window; not standard Modbus register quantities.
* AT25256B keeps its existing 0x8000..0xBFFF / count 1..120 requests.
* M95M02 uses four 64-KiB windows: count bit15 marks the profile, bits9:8
* select bank 0..3, bits7:0 carry word count. Address 0x8000..0xFFFF is
* the word offset within a bank. Every request is self-contained; no global
* bank register can be changed by another client. Old firmware rejects the
* marked quantity. Neither profile silently reads the other memory type.
*/
#ifndef BALSAM167_EEPROM_BLOCK_H
#define BALSAM167_EEPROM_BLOCK_H
#include "balsam167_eeprom_layout.h"
#define BALSAM167_EEPROM_RTU_BASE 0x8000U
#define BALSAM167_EEPROM_BLOCK_WORDS 120U
#define BALSAM167_EEPROM_M95_MARKER 0x8000U
#define BALSAM167_EEPROM_WRITE_M95 0x44U
static unsigned long balsam167_eeprom_capacity(unsigned profile)
{
return profile == 0 ? 32768UL : (profile == 1 ? 262144UL : 0UL);
}
static unsigned long balsam167_eeprom_block_word(unsigned address, unsigned count)
{
return ((unsigned long)(count & 0x0300U) << 7) + (address - 0x8000U);
}
static unsigned balsam167_eeprom_block_valid_profile(unsigned fn, unsigned address,
unsigned count, unsigned profile)
{
unsigned n = count & 255U;
unsigned long word;
if (fn != 3 || address < 0x8000U || !n || n > 120U) return 0;
if (profile == 0) return count == n && address < 0xC000U && n <= 0xC000U-address;
if (profile != 1 || (count & 0xFC00U) != 0x8000U) return 0;
word = balsam167_eeprom_block_word(address, count);
return n <= 0x10000UL-address && word+n <= 131072UL;
}
static unsigned balsam167_eeprom_block_valid(unsigned fn, unsigned address, unsigned count)
{
#if EEPROM_M95M02
return balsam167_eeprom_block_valid_profile(fn, address, count, 1);
#else
return fn == 3 && address >= 0x8000U && address < 0xC000U &&
count && count <= 120U && count <= 0xC000U-address;
#endif
}
#endif

View File

@@ -5,8 +5,20 @@
*/
#ifndef BALSAM167_EEPROM_LAYOUT_H
#define BALSAM167_EEPROM_LAYOUT_H
#define BALSAM167_EEPROM_BYTES 0x8000
/* EEPROM_M95M02 is selected by the board configuration, never autodetected
* by writing test patterns. Keep numeric constants usable by the TI linker. */
#define BALSAM167_AT25256_BYTES 0x8000
#define BALSAM167_M95M02_BYTES 0x40000
#ifndef EEPROM_M95M02
#define EEPROM_M95M02 0
#endif
#if EEPROM_M95M02
#define BALSAM167_EEPROM_BYTES BALSAM167_M95M02_BYTES
#define BALSAM167_EEPROM_PAGE_BYTES 256
#else
#define BALSAM167_EEPROM_BYTES BALSAM167_AT25256_BYTES
#define BALSAM167_EEPROM_PAGE_BYTES 64
#endif
#define BALSAM167_EEPROM_PARAM_WORDS 129
#define BALSAM167_EEPROM_WORDS (BALSAM167_EEPROM_BYTES / 2)
/* Preserve the historical end-exclusive parameter boundary. The final word

View File

@@ -91,6 +91,16 @@ PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_request(const void *state,
PCAN_ABI_API int pcan_abi_periph28335_eeprom_accept(void *state,
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity(void);
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned profile);
PCAN_ABI_API int pcan_abi_periph28335_eeprom_begin_profile(void *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *r, unsigned profile);
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *r, uint8_t *o, size_t n, unsigned profile);
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept_profile(void *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected(const void *state);
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request(const void *state, uint8_t *out, size_t cap);
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept(void *state, const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
PCAN_ABI_API size_t pcan_abi_periph28335_append_crc(
const uint8_t *payload, size_t payload_size,
uint8_t *output, size_t output_size);

View File

@@ -50,6 +50,22 @@ size_t periph28335_eeprom_request(const periph28335_eeprom_reader *reader,
int periph28335_eeprom_accept(periph28335_eeprom_reader *reader,
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
/* FC03 block reader uses the same state/physical byte range as legacy PEEK.
* expected() is the exact response length. No state advance on CRC/size error. */
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r);
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap);
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r,
const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
/* Profile 0 = AT25256B, 1 = M95M02; stable additional API for every host port.
* State remains four uint32_t fields. M95 requires block mode, no PEEK fallback. */
uint32_t periph28335_eeprom_capacity_profile(unsigned profile);
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile);
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *o, size_t n, unsigned profile);
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
typedef enum {
PERIPH28335_OK = 0,
PERIPH28335_ERROR_ARGUMENT = -1,

View File

@@ -380,6 +380,26 @@ int pcan_abi_gui_parser_stats(const void *parser_storage, uint32_t *frames,
return 1;
}
size_t pcan_abi_periph28335_eeprom_block_expected(const void *s)
{ return periph28335_eeprom_block_expected((const periph28335_eeprom_reader *)s); }
size_t pcan_abi_periph28335_eeprom_block_request(const void *s, uint8_t *out, size_t cap)
{ return periph28335_eeprom_block_request((const periph28335_eeprom_reader *)s,out,cap); }
int pcan_abi_periph28335_eeprom_block_accept(void *s, const uint8_t *reply, size_t size, uint8_t *out, size_t cap)
{ return periph28335_eeprom_block_accept((periph28335_eeprom_reader *)s,reply,size,out,cap); }
uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned p)
{ return periph28335_eeprom_capacity_profile(p); }
int pcan_abi_periph28335_eeprom_begin_profile(void *s, uint32_t c, uint32_t a, uint32_t n, unsigned p)
{ return periph28335_eeprom_begin_profile((periph28335_eeprom_reader *)s,c,a,n,p); }
size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *s, unsigned p)
{ return periph28335_eeprom_block_expected_profile((const periph28335_eeprom_reader *)s,p); }
size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *s, uint8_t *o, size_t n, unsigned p)
{ return periph28335_eeprom_block_request_profile((const periph28335_eeprom_reader *)s,o,n,p); }
int pcan_abi_periph28335_eeprom_block_accept_profile(void *s, const uint8_t *r, size_t n, uint8_t *o, size_t cap, unsigned p)
{ return periph28335_eeprom_block_accept_profile((periph28335_eeprom_reader *)s,r,n,o,cap,p); }
size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t c, uint32_t d, uint32_t s, uint32_t n, unsigned p, uint8_t *o, size_t cap)
{ return periph28335_eeprom_write_profile(c,d,s,n,p,o,cap); }
int pcan_abi_tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command)
{

View File

@@ -9,22 +9,24 @@
#include <string.h>
#include "set_crc.h"
#include "balsam167_eeprom_block.h"
static int eeprom_reader_valid(const periph28335_eeprom_reader *r, unsigned profile)
static int eeprom_reader_valid(const periph28335_eeprom_reader *r)
{
return r && r->controller >= 1U && r->controller <= 255U &&
r->start < PERIPH28335_EEPROM_BYTES && r->length &&
r->length <= PERIPH28335_EEPROM_BYTES - r->start && r->received < r->length;
r->start < balsam167_eeprom_capacity(profile) && r->length &&
r->length <= balsam167_eeprom_capacity(profile) - r->start && r->received < r->length;
}
int periph28335_eeprom_begin(periph28335_eeprom_reader *r,
uint32_t controller, uint32_t start, uint32_t length)
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r,
uint32_t controller, uint32_t start, uint32_t length, unsigned profile)
{
if (!r) return PERIPH28335_ERROR_ARGUMENT;
/* Invalidate first: a rejected new range must not leave an old read active. */
memset(r, 0, sizeof(*r));
if (!controller || controller > 255U || start >= PERIPH28335_EEPROM_BYTES ||
!length || length > PERIPH28335_EEPROM_BYTES - start)
if (!controller || controller > 255U || start >= balsam167_eeprom_capacity(profile) ||
!length || length > balsam167_eeprom_capacity(profile) - start)
return PERIPH28335_ERROR_RANGE;
r->controller = controller; r->start = start; r->length = length;
return PERIPH28335_OK;
@@ -35,7 +37,7 @@ size_t periph28335_eeprom_request(const periph28335_eeprom_reader *r,
{
uint8_t payload[6];
uint32_t word;
if (!eeprom_reader_valid(r)) return 0;
if (!eeprom_reader_valid(r, 0)) return 0;
word = (r->start + r->received) / 2U;
payload[0] = (uint8_t)r->controller; payload[1] = 56U; /* CMD_PEEK only */
/* 167 routes words 0..0x3fff to SPI EEPROM. The old terminal's
@@ -49,7 +51,7 @@ int periph28335_eeprom_accept(periph28335_eeprom_reader *r,
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity)
{
unsigned odd, count;
if (!eeprom_reader_valid(r) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT;
if (!eeprom_reader_valid(r, 0) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT;
/* The UART layer may strip two legacy zero-padding bytes after CRC. */
if (size != 8U && size != 10U) return PERIPH28335_ERROR_LENGTH;
if (periph28335_crc16_modbus(reply, 8U) != 0) return PERIPH28335_ERROR_CRC;
@@ -64,6 +66,87 @@ int periph28335_eeprom_accept(periph28335_eeprom_reader *r,
return (int)count;
}
/* One common codec for all host ports. Physical EEPROM bytes already have
* network-word order; only trim an odd first/last byte, never swap them. */
static unsigned eeprom_block_words(const periph28335_eeprom_reader *r, unsigned profile)
{
uint32_t n;
if (!eeprom_reader_valid(r, profile)) return 0;
n = (r->length - r->received + ((r->start + r->received) & 1U) + 1U) / 2U;
/* A response cannot cross a bank boundary; trim the last block there. */
if (n > 32768U - ((r->start+r->received)/2U % 32768U))
n = 32768U - ((r->start+r->received)/2U % 32768U);
return (unsigned)(n > BALSAM167_EEPROM_BLOCK_WORDS ? BALSAM167_EEPROM_BLOCK_WORDS : n);
}
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile)
{
unsigned n = eeprom_block_words(r, profile);
return n ? 5U + 2U*n : 0;
}
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap, unsigned profile)
{
uint8_t p[6];
unsigned n = eeprom_block_words(r, profile), a, quantity;
uint32_t word;
if (!n) return 0;
word = (r->start + r->received) / 2U;
a = 0x8000U + (unsigned)(word % 32768U);
quantity = n | (profile ? (0x8000U | (unsigned)(word/32768U)*256U) : 0U);
if (!balsam167_eeprom_block_valid_profile(3,a,quantity,profile)) return 0;
p[0]=(uint8_t)r->controller; p[1]=3;
p[2]=(uint8_t)(a>>8); p[3]=(uint8_t)a; p[4]=(uint8_t)(quantity>>8); p[5]=(uint8_t)quantity;
return periph28335_append_crc(p,6,out,cap);
}
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r,
const uint8_t *reply, size_t size, uint8_t *out, size_t cap, unsigned profile)
{
unsigned n=eeprom_block_words(r, profile), odd, count;
if (!n || !reply || !out) return PERIPH28335_ERROR_ARGUMENT;
if (size != 5U+2U*n) return PERIPH28335_ERROR_LENGTH;
if (periph28335_crc16_modbus(reply,size)) return PERIPH28335_ERROR_CRC;
/* Common address 16 is answered by the board's own Mode, 5 or 6. */
if ((reply[0] != r->controller && !(r->controller==16 && (reply[0]==5 || reply[0]==6))) ||
reply[1]!=3 || reply[2]!=2U*n) return PERIPH28335_ERROR_HEADER;
odd=(unsigned)((r->start+r->received)&1U);
count=2U*n-odd;
if (count>r->length-r->received) count=(unsigned)(r->length-r->received);
if (cap<count) return PERIPH28335_ERROR_CAPACITY;
memcpy(out,reply+3+odd,count);
r->received+=count;
return (int)count;
}
/* Keep all existing exported symbols and the 16-byte reader ABI unchanged. */
int periph28335_eeprom_begin(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n)
{ return periph28335_eeprom_begin_profile(r,c,a,n,0); }
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r)
{ return periph28335_eeprom_block_expected_profile(r,0); }
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *o, size_t n)
{ return periph28335_eeprom_block_request_profile(r,o,n,0); }
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap)
{ return periph28335_eeprom_block_accept_profile(r,p,n,o,cap,0); }
uint32_t periph28335_eeprom_capacity_profile(unsigned profile)
{ return (uint32_t)balsam167_eeprom_capacity(profile); }
/* Legacy EXTEND carries word addresses and octet lengths. A distinct M95
* subcommand makes a wrong-profile write fail closed on either firmware. */
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination,
uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap)
{
uint8_t p[16]; unsigned i;
uint32_t capacity = periph28335_eeprom_capacity_profile(profile);
uint32_t limit = capacity ? capacity-(BALSAM167_EEPROM_PARAM_WORDS+1U)*2U : 0;
if (!controller || controller>255U || !length || length>199993U ||
destination>=limit/2U || length>limit-destination*2U) return 0;
p[0]=(uint8_t)controller; p[1]=60;
for(i=0;i<4;i++) {
p[2+i]=(uint8_t)(destination>>(8*i));
p[6+i]=(uint8_t)(source>>(8*i));
p[10+i]=(uint8_t)(length>>(8*i));
}
p[14]=(uint8_t)(profile ? BALSAM167_EEPROM_WRITE_M95 : 4); p[15]=0;
return periph28335_append_crc(p,16,out,cap);
}
typedef struct {
const char *project;
const char *commands[PERIPH28335_COMMAND_COUNT];

View File

@@ -1,5 +1,6 @@
#include "periph28335.h"
#include <assert.h>
#include "balsam167_eeprom_block.h"
#include <string.h>
static void response(uint8_t *out, unsigned high, unsigned low)
@@ -12,9 +13,100 @@ static void response(uint8_t *out, unsigned high, unsigned low)
out[8]=out[9]=0;
}
static void blocks(void)
{
periph28335_eeprom_reader r;
uint8_t req[8], reply[245], out[240];
unsigned start, transactions, n, a, i, old;
int got;
assert(!balsam167_eeprom_block_valid(6,0x8000,1));
assert(!balsam167_eeprom_block_valid(3,0xbfff,2));
assert(!balsam167_eeprom_block_valid(3,0xffff,120));
assert(!balsam167_eeprom_block_valid(3,0x8000,121));
assert(!balsam167_eeprom_block_valid(3,0x8000,0));
assert(balsam167_eeprom_block_valid(3,0xbfff,1));
for(start=0;start<2;start++) {
assert(!periph28335_eeprom_begin(&r,16,start,32768-start));
transactions=0;
while(r.received<r.length) {
assert(periph28335_eeprom_block_request(&r,req,8)==8);
assert(!periph28335_crc16_modbus(req,8));
a=((unsigned)req[2]*256+req[3]-0x8000)*2;
n=req[5]; assert(n && n<=120 && a+2*n<=32768);
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(2*n);
for(i=0;i<2*n;i++) reply[3+i]=(uint8_t)(a+i);
assert(periph28335_append_crc(reply,3+2*n,reply,sizeof reply)==5+2*n);
old=r.received;
reply[4]^=1;
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240)==PERIPH28335_ERROR_CRC);
assert(r.received==old); reply[4]^=1;
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,0)==PERIPH28335_ERROR_CAPACITY);
assert(r.received==old);
got=periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240);
assert(got>0);
for(i=0;i<(unsigned)got;i++) assert(out[i]==(uint8_t)(start+old+i));
transactions++;
}
assert(transactions==137);
assert(!periph28335_eeprom_block_request(&r,req,8));
}
}
static void profiles(void)
{
periph28335_eeprom_reader r;
uint8_t req[18], reply[245], out[240];
unsigned n, i, q, a, bankmask=0;
uint32_t pos, before;
assert(periph28335_eeprom_capacity_profile(0)==32768);
assert(periph28335_eeprom_capacity_profile(1)==262144);
assert(periph28335_eeprom_capacity_profile(2)==0);
assert(periph28335_eeprom_begin_profile(&r,16,262143,2,1)<0);
assert(periph28335_eeprom_begin_profile(&r,16,0,2,2)<0);
assert(!balsam167_eeprom_block_valid_profile(3,0xffff,0x8078,1));
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8401,1));
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8001,0));
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,1,1));
assert(balsam167_eeprom_block_valid_profile(3,0xffff,0x8301,1));
/* Full 256-KiB dump from an odd start exercises every bank transition,
* first/last byte trimming, CRC failure and exact physical byte order. */
assert(!periph28335_eeprom_begin_profile(&r,16,1,262143,1));
while(r.received<r.length) {
assert(periph28335_eeprom_block_request_profile(&r,req,8,1)==8);
assert(!periph28335_crc16_modbus(req,8));
a=req[2]*256U+req[3]; q=req[4]*256U+req[5]; n=q&255U;
assert(balsam167_eeprom_block_valid_profile(3,a,q,1));
pos=balsam167_eeprom_block_word(a,q)*2UL;
assert(pos == (r.start+r.received)/2*2);
bankmask |= 1U << ((q>>8)&3U);
assert(periph28335_eeprom_block_expected_profile(&r,1)==5+2*n);
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(n*2);
for(i=0;i<n*2;i++) reply[3+i]=(uint8_t)((pos+i)^((pos+i)>>16));
periph28335_append_crc(reply,3+n*2,reply,sizeof(reply));
before=r.received; reply[4]^=1;
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)==PERIPH28335_ERROR_CRC);
assert(r.received==before); reply[4]^=1;
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)>0);
for(i=0;i<r.received-before;i++) {
pos=1+before+i;
assert(out[i]==(uint8_t)(pos^(pos>>16)));
}
}
assert(bankmask==15);
assert(!periph28335_eeprom_block_request_profile(&r,req,8,1));
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,0,req,18)==0);
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,1,req,18)==18);
assert(req[14]==0x44 && req[15]==0 && !periph28335_crc16_modbus(req,18));
assert(!periph28335_eeprom_write_profile(5,0x1ff7e,0,2,1,req,18));
assert(!periph28335_eeprom_write_profile(5,0xffffffff,0,2,1,req,18));
assert(periph28335_eeprom_write_profile(5,0,0,32438,0,req,18)==18 && req[14]==4);
}
int main(void)
{
periph28335_eeprom_reader reader;
blocks();
profiles();
uint8_t request[8], reply[10], data[2];
unsigned i;
assert(sizeof(reader)==16);

View File

@@ -1,4 +1,4 @@
# Tektronix DPO4034 / MDO4034C
# Tektronix DPO4034 / DPO4034B / MDO4034C
Независимый от Qt драйвер: `python/set_devices/tektronix.py`.
Установка: `pip install "setcorp-templates[tektronix]"` из пакета templates.
@@ -18,7 +18,7 @@
## Реализованный обмен
Команды SCPI с LF. После открытия `*IDN?` проверяет производителя и точную
модель DPO4034 или MDO4034C; `HEADER OFF` отключает заголовки ответов.
модель DPO4034, DPO4034B или MDO4034C; `HEADER OFF` отключает заголовки ответов.
Для снимка запрашивается `ACQUIRE:STATE?`, затем отправляется
`ACQUIRE:STATE STOP` и ожидается `*OPC?`. Это позволяет читать несколько

View File

@@ -20,20 +20,42 @@ def _api(name, args, result):
return function
def capacity():
return int(_api("capacity", [], ct.c_uint32)())
def capacity(m95m02=False):
return int(_api("capacity_profile", [ct.c_uint], ct.c_uint32)(int(m95m02)))
def write_request(controller, destination, source, length, *, m95m02=False):
values = (controller, destination, source, length)
if any(type(v) is not int or not 0 <= v <= 0xffffffff for v in values):
raise ValueError("Адрес или длина вне диапазона uint32")
output = ct.create_string_buffer(18)
size = _api("write_profile", [ct.c_uint32]*4 + [ct.c_uint, ct.c_void_p, ct.c_size_t], ct.c_size_t)(
*values, int(m95m02), output, len(output))
if not size:
raise ValueError("LengthLoad или адрес выходит за область прошивки выбранной EEPROM либо пересекает область параметров. "
"Предел от адреса 0: AT25256B — 32508 байт, M95M02 — 261884 байт.")
return output.raw[:size]
class Reader:
def __init__(self, controller, start, length):
def __init__(self, controller, start, length, *, block=False, m95m02=False):
self.m95m02 = bool(m95m02)
if m95m02 and not block:
raise ValueError("Для M95M02 необходимо блочное чтение")
self.block = block
for value in (controller, start, length):
if type(value) is not int or not 0 <= value <= 0xffffffff:
raise ValueError("Адрес и длина должны быть неотрицательными целыми числами")
self._state = _State()
result = _api("begin", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32], ct.c_int)(
ct.byref(self._state), controller, start, length)
result = _api("begin_profile", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32, ct.c_uint], ct.c_int)(
ct.byref(self._state), controller, start, length, int(self.m95m02))
if result:
raise ValueError("Диапазон вне EEPROM AT25256B: 0…0x7FFF байт; длина должна быть больше нуля")
raise ValueError(f"Диапазон вне выбранной EEPROM: 0…0x{capacity(self.m95m02)-1:X}; длина должна быть больше нуля")
if block:
# Detect an old DLL before the GUI acquires ownership of the port.
_api("block_request_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_size_t)
_api("block_accept_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_int)
self.expected
@property
def received(self): return int(self._state.received)
@@ -42,17 +64,26 @@ class Reader:
@property
def start(self): return int(self._state.start)
@property
def expected(self):
if not self.block: return 8
return int(_api("block_expected_profile", [ct.c_void_p, ct.c_uint], ct.c_size_t)(ct.byref(self._state), int(self.m95m02)))
def request(self):
output = ct.create_string_buffer(8)
size = _api("request", [ct.c_void_p, ct.c_void_p, ct.c_size_t], ct.c_size_t)(
ct.byref(self._state), output, len(output))
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
extra = [int(self.m95m02)] if self.block else []
size = _api("block_request_profile" if self.block else "request", args, ct.c_size_t)(
ct.byref(self._state), output, len(output), *extra)
return output.raw[:size]
def accept(self, reply):
output = ct.create_string_buffer(2)
output = ct.create_string_buffer(240 if self.block else 2)
reply = bytes(reply)
size = _api("accept", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t], ct.c_int)(
ct.byref(self._state), reply, len(reply), output, len(output))
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
extra = [int(self.m95m02)] if self.block else []
size = _api("block_accept_profile" if self.block else "accept", args, ct.c_int)(
ct.byref(self._state), reply, len(reply), output, len(output), *extra)
if size < 0:
errors = {-3: "Неверная длина ответа EEPROM", -4: "Ошибка CRC ответа EEPROM",
-5: "Неверный адрес контроллера или команда ответа EEPROM"}

View File

@@ -8,6 +8,6 @@ Shared PySide6 terminal migrated from PySetTerminal-master/pysetterminal.
- COM transport uses QtSerialPort. Embedded operations reuse the TMS2812 connection and configured controller address.
- PEEK/POKE packet construction and response validation use the existing protocan native C ABI, with 16-bit TMS2812 values.
- Macro files are stored under the application data directory, outside the source checkout.
- PySide2 and pyserial are no longer required. A native set-protocol runtime remains required.
- The standalone launcher uses PySide6; SETGUI Windows 7 uses its existing PySide2/Qt5 compatibility layer. Frozen SETGUI builds bundle Python and Qt. Pyserial is not required. A native set-protocol runtime remains required.
`build_and_clean.py` is the migrated original standalone packaging helper.

View File

@@ -1,4 +1,5 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QGroupBox, QGridLayout, QLineEdit, QPushButton, QScrollArea
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QGroupBox, QGridLayout, QLineEdit, QPushButton, QScrollArea, QSizePolicy
def build_byte_page(self):
self.byte_page = QScrollArea()
@@ -13,18 +14,22 @@ def build_byte_page(self):
panels = QHBoxLayout()
for write in (True, False):
group = QGroupBox("Запись" if write else "Чтение")
group.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
form = QGridLayout(group)
address = QLineEdit("$1000")
value = QLineEdit("0000" if write else "")
binary = QLineEdit("0000 0000 0000 0000" if write else "")
binary.setMinimumWidth(220)
address.setFixedWidth(150)
value.setFixedWidth(100)
binary.setFixedWidth(240)
value.setReadOnly(not write)
binary.setReadOnly(not write)
for row, (title, widget) in enumerate((("Адрес", address), ("Байт (hex)", value), ("Байт (bin)", binary))):
form.addWidget(QLabel(title), row * 2, 0)
form.addWidget(widget, row * 2 + 1, 0)
form.addWidget(QLabel(title), row, 0)
form.addWidget(widget, row, 1)
button = QPushButton("Записать" if write else "Прочитать")
form.addWidget(button, 6, 0)
form.addWidget(button, 3, 1, Qt.AlignLeft)
form.setColumnStretch(2, 1)
if write:
self.byte_write_address, self.byte_write_value, self.byte_write_binary = address, value, binary
value.textEdited.connect(lambda text: self._sync_byte_value(text, 16))
@@ -34,6 +39,7 @@ def build_byte_page(self):
self.byte_read_address, self.byte_read_value, self.byte_read_binary = address, value, binary
button.clicked.connect(lambda: self._start_byte(False))
panels.addWidget(group)
panels.addStretch()
layout.addLayout(panels)
status = QLabel("Нет данных от контроллера")
status.setWordWrap(True)

View File

@@ -229,6 +229,7 @@ class RawProtocolWidget(QWidget):
self.mem_result_label = QLabel("---")
mem_layout.addWidget(self.mem_result_label, 1, 3)
mem_layout.setColumnStretch(4, 1)
mem_group.setLayout(mem_layout)
layout.addWidget(mem_group)
@@ -238,6 +239,7 @@ class RawProtocolWidget(QWidget):
self.raw_send_edit = QLineEdit()
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
self.raw_send_edit.setMaximumWidth(640)
raw_layout.addWidget(self.raw_send_edit)
hbox = QHBoxLayout()
@@ -250,6 +252,7 @@ class RawProtocolWidget(QWidget):
hbox.addWidget(self.raw_read_btn)
hbox.addStretch()
raw_layout.addLayout(hbox)
self.raw_output = QTextEdit()
@@ -260,7 +263,7 @@ class RawProtocolWidget(QWidget):
self.raw_clear_btn = QPushButton("Очистить логи")
self.raw_clear_btn.clicked.connect(self.clear_logs)
raw_layout.addWidget(self.raw_clear_btn)
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
raw_group.setLayout(raw_layout)
layout.addWidget(raw_group)

View File

@@ -1,11 +1,56 @@
import json
import os
import weakref
from functools import partial
from copy import deepcopy
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class _MacroActionIcon(QIconEngine):
"""Scalable action icons, painted with the button's current theme color."""
def __init__(self, action, button):
super().__init__()
self.action = action
self.button = weakref.ref(button)
def clone(self):
return _MacroActionIcon(self.action, self.button())
def paint(self, painter, rect, mode, state):
button = self.button()
palette = button.palette() if button is not None else QApplication.palette()
group = QPalette.Disabled if mode == QIcon.Disabled else QPalette.Active
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
side = min(rect.width(), rect.height())
painter.translate(rect.x() + (rect.width() - side) / 2, rect.y() + (rect.height() - side) / 2)
painter.scale(side / 24, side / 24)
painter.setPen(QPen(palette.color(group, QPalette.ButtonText), 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.setBrush(Qt.NoBrush)
if self.action == "edit":
path = QPainterPath(QPointF(4, 16))
for x, y in ((15, 5), (19, 9), (8, 20), (3, 21), (4, 16)):
path.lineTo(x, y)
painter.drawPath(path)
painter.drawLine(QPointF(13, 7), QPointF(17, 11))
else:
painter.drawLine(QPointF(4, 6), QPointF(20, 6))
painter.drawRoundedRect(QRectF(6, 6, 12, 15), 1.5, 1.5)
painter.drawPolyline(QPolygonF([QPointF(9, 6), QPointF(9, 3), QPointF(15, 3), QPointF(15, 6)]))
painter.drawLine(QPointF(10, 10), QPointF(10, 17))
painter.drawLine(QPointF(14, 10), QPointF(14, 17))
painter.restore()
def pixmap(self, size, mode, state):
pixmap = QPixmap(size)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
self.paint(painter, QRect(QPoint(), size), mode, state)
painter.end()
return pixmap
class SerialBus:
"""ВЫСОКОУРОВНЕВЫЙ протокол для работы с серийной шиной (только через регистры)"""
@@ -320,12 +365,17 @@ class SerialTab(QWidget):
self.bus_write_btn = QPushButton("Записать в шину")
self.bus_write_btn.clicked.connect(self.write_to_bus)
bus_layout.addWidget(self.bus_write_btn, 1, 0, 1, 3)
self.bus_read_btn = QPushButton("Прочитать из шину")
self.bus_read_btn.clicked.connect(self.read_from_bus)
bus_layout.addWidget(self.bus_read_btn, 1, 3, 1, 3)
bus_buttons = QHBoxLayout()
bus_buttons.setSpacing(6)
bus_buttons.addWidget(self.bus_write_btn)
bus_buttons.addWidget(self.bus_read_btn)
bus_buttons.addStretch(1)
bus_layout.addLayout(bus_buttons, 1, 0, 1, 6)
bus_layout.setColumnStretch(6, 1)
bus_group.setLayout(bus_layout)
layout.addWidget(bus_group)
@@ -336,23 +386,34 @@ class SerialTab(QWidget):
# Кнопка создания нового макроса
self.create_macro_btn = QPushButton("Создать новый макрос")
self.create_macro_btn.clicked.connect(self.create_macro)
macros_layout.addWidget(self.create_macro_btn)
self.stop_macro_btn = QPushButton("Остановить макрос")
self.stop_macro_btn.setEnabled(False)
self.stop_macro_btn.clicked.connect(self.stop_macro)
macros_layout.addWidget(self.stop_macro_btn)
macro_actions = QHBoxLayout()
macro_actions.setSpacing(6)
macro_actions.addWidget(self.create_macro_btn)
macro_actions.addWidget(self.stop_macro_btn)
macro_actions.addStretch(1)
macros_layout.addLayout(macro_actions)
for button in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, self.stop_macro_btn):
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
button.setFixedHeight(30)
button.setStyleSheet("QPushButton { padding: 3px 10px; }")
# Прокручиваемая область для кнопок макросов
self.macros_scroll_area = QScrollArea()
self.macros_scroll_area.setWidgetResizable(True)
# Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов
self.macros_scroll_area.setMinimumHeight(150) # Высота для 3-4 строк
self.macros_scroll_area.setMinimumHeight(120) # Высота для 3-4 строк
self.macros_scroll_content = QWidget()
self.macros_scroll_layout = QGridLayout()
self.macros_scroll_content.setLayout(self.macros_scroll_layout)
self.macros_scroll_area.setWidget(self.macros_scroll_content)
self.macros_scroll_layout.setAlignment(Qt.AlignTop)
self.macros_scroll_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.macros_scroll_layout.setContentsMargins(6, 6, 6, 6)
self.macros_scroll_layout.setHorizontalSpacing(8)
self.macros_scroll_layout.setVerticalSpacing(6)
macros_layout.addWidget(self.macros_scroll_area)
macros_group.setLayout(macros_layout)
@@ -375,6 +436,9 @@ class SerialTab(QWidget):
results_layout.addWidget(self.bus_result_label, 1, 1)
# Создаем фрейм для результатов
results_layout.setColumnStretch(2, 1)
self.bus_read_value_label.setMinimumWidth(180)
self.bus_result_label.setMaximumWidth(640)
results_frame = QFrame()
results_frame.setFrameShape(QFrame.StyledPanel)
results_frame.setLayout(results_layout)
@@ -470,8 +534,7 @@ class SerialTab(QWidget):
def add_macro_button_with_index(self, macro, index):
"""Добавить кнопку макроса в интерфейс с указанным индексом"""
# Изменяем расчет строк и колонок для 3 колонок
items_per_row = 3 # Теперь 3 кнопки в строке
items_per_row = 2
row = len(self.macro_buttons) // items_per_row
col = len(self.macro_buttons) % items_per_row
@@ -479,18 +542,20 @@ class SerialTab(QWidget):
container = QWidget()
container_layout = QHBoxLayout()
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(4)
container.setMaximumWidth(360)
container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
# Основная кнопка макроса
macro_btn = QPushButton(macro['name'])
macro_btn.setMinimumHeight(40)
macro_btn.setFixedHeight(30)
macro_btn.setMinimumWidth(120)
macro_btn.setToolTip(macro["name"])
macro_btn.setStyleSheet("""
QPushButton {
font-weight: bold;
text-align: left;
padding: 10px;
}
QPushButton:hover {
background-color: #e0e0e0;
padding: 3px 8px;
}
""")
@@ -498,14 +563,22 @@ class SerialTab(QWidget):
macro_btn.clicked.connect(partial(self.execute_macro_by_index, index))
# Кнопка редактирования
edit_btn = QPushButton("✎")
edit_btn = QPushButton()
edit_btn.setIcon(QIcon(_MacroActionIcon("edit", edit_btn)))
edit_btn.setIconSize(QSize(16, 16))
edit_btn.setFixedSize(30, 30)
edit_btn.setStyleSheet("QPushButton { padding: 0; }")
edit_btn.setAccessibleName("Редактировать макрос")
edit_btn.setToolTip("Редактировать макрос")
edit_btn.clicked.connect(partial(self.edit_macro_by_index, index))
# Кнопка удаления
delete_btn = QPushButton("✕")
delete_btn = QPushButton()
delete_btn.setIcon(QIcon(_MacroActionIcon("delete", delete_btn)))
delete_btn.setIconSize(QSize(16, 16))
delete_btn.setFixedSize(30, 30)
delete_btn.setStyleSheet("QPushButton { padding: 0; }")
delete_btn.setAccessibleName("Удалить макрос")
delete_btn.setToolTip("Удалить макрос")
delete_btn.clicked.connect(partial(self.delete_macro, index))

View File

@@ -0,0 +1,64 @@
"""Renderer-independent comparison of arbitrary binary files."""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
def compare_sha256(image: bytes, readback: bytes):
"""Compare a complete physical readback against an immutable BIN snapshot."""
if not image or len(image) != len(readback):
raise ValueError('Для SHA-256 нужен полный прочитанный диапазон размером с BIN.')
expected = hashlib.sha256(image).hexdigest()
actual = hashlib.sha256(readback).hexdigest()
return expected == actual, expected, actual
@dataclass(frozen=True)
class Comparison:
left: bytes
right: bytes
offsets: tuple[int, ...]
changed_bits: int
@property
def changed_bytes(self) -> int:
return len(self.offsets)
@property
def size(self) -> int:
return max(len(self.left), len(self.right))
def compare(left: bytes, right: bytes) -> Comparison:
"""Count bit differences in overlap; unmatched bytes count as changed bytes."""
common = min(len(left), len(right))
offsets = tuple(i for i in range(common) if left[i] != right[i])
bits = sum(bin(left[i] ^ right[i]).count('1') for i in offsets)
return Comparison(left, right, offsets + tuple(range(common, max(len(left), len(right)))), bits)
@dataclass(frozen=True)
class MultiComparison:
images: tuple[bytes, ...]
offsets: tuple[int, ...]
pairs: tuple
@property
def size(self): return max(map(len, self.images))
@property
def changed_bytes(self): return len(self.offsets)
def compare_many(images):
"""Highlight offsets where any pair differs, including missing bytes."""
images = tuple(images)
if len(images) not in (1, 2, 3):
raise ValueError('Выберите от 1 до 3 BIN-файлов.')
if len(images) == 2:
return compare(*images)
pairs = tuple((a, b, compare(images[a], images[b]))
for a in range(len(images)) for b in range(a + 1, len(images)))
offsets = tuple(sorted({offset for _, _, result in pairs for offset in result.offsets}))
return MultiComparison(images, offsets, pairs)

View File

@@ -0,0 +1,107 @@
"""TI C28x tool adapter. Decode instructions using TI's own assembler tools."""
from pathlib import Path
import subprocess
import tempfile
import os
import re
def align_disassembly(*texts):
"""Align TI listings by word offset even when instruction sizes differ."""
listings = []
for text in texts:
if text.startswith('Ошибка:'):
return texts
listing = {}
previous = None
for line in text.splitlines():
match = re.match(r'^([0-9A-Fa-f]{8})\s+', line)
if match:
previous = int(match.group(1), 16)
listing[previous] = line
elif previous is not None and line.strip():
listing[previous] += ' ' + line.strip()
if not listing:
return texts
listings.append(listing)
addresses = sorted({address for listing in listings for address in listing})
return tuple('\n'.join(listing.get(address, f'{address:08X} —') for address in addresses)
for listing in listings)
def find_ghidra(root):
configured = os.environ.get('GHIDRA_INSTALL_DIR', '')
if configured and (Path(configured) / 'support/analyzeHeadless.bat').is_file():
return configured
return next((str(path) for path in sorted(Path(root).glob('ghidra_*'), reverse=True)
if (path / 'support/analyzeHeadless.bat').is_file()), '')
def decompile_c28x(data: bytes, tools: str, base: int = 0) -> str:
"""Run the configured Ghidra C28x processor and decompiler locally."""
root = Path(tools)
headless = root / 'support/analyzeHeadless.bat'
if not tools or not headless.is_file():
raise ValueError('Укажите папку Ghidra с установленным процессором TMS320C28x.')
if not data or len(data) % 2 or len(data) > 262144:
raise ValueError('Нужен непустой чётный диапазон до 256 КиБ.')
if not 0 <= base <= 0x3fffff:
raise ValueError('Адрес C28x должен быть в диапазоне 0…0x3FFFFF (в словах).')
scripts = Path(__file__).parent / 'disassembly_scripts'
env = dict(os.environ)
jdks = sorted(path for path in root.parent.glob('jdk*')
if path.is_dir() and (path / 'bin/java.exe').is_file())
if jdks:
env['JAVA_HOME'] = str(jdks[-1])
with tempfile.TemporaryDirectory(prefix='setgui-decompile-') as folder:
work = Path(folder)
binary, output = work / 'range.bin', work / 'result.c'
binary.write_bytes(data)
arguments = [str(headless), str(work), 'analysis', '-import', str(binary),
'-processor', 'TMS320C28x:LE:32:default', '-loader', 'BinaryLoader',
'-loader-baseAddr', f'{base:x}', '-noanalysis', '-scriptPath', str(scripts),
'-postScript', 'SetguiExportC.java', str(output), '-deleteProject']
result = subprocess.run(arguments, env=env, capture_output=True, text=True,
errors='replace', timeout=180, stdin=subprocess.DEVNULL,
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
if result.returncode or not output.is_file():
raise RuntimeError((result.stderr + '\n' + result.stdout)[-6000:])
text = output.read_text(encoding='utf-8')
if not text.rstrip().endswith('/* SETGUI_DECOMPILE_SUCCESS */'):
raise RuntimeError('Декомпиляция не завершена. ' + (result.stderr + '\n' + result.stdout)[-6000:])
return text.removesuffix('/* SETGUI_DECOMPILE_SUCCESS */\n')
def find_c28x_tools():
candidates = sorted(Path('C:/ti').glob('ti-cgt-c2000_*/bin'), reverse=True)
candidates += sorted(Path('C:/ti').glob('ccs*/ccs/tools/compiler/ti-cgt-c2000_*/bin'), reverse=True)
return next((str(path) for path in candidates
if (path / 'cl2000.exe').is_file() and (path / 'dis2000.exe').is_file()), '')
def disassemble_c28x(data: bytes, tools: str) -> str:
"""Wrap raw little-endian words in a temporary text section for dis2000."""
if not data or len(data) % 2:
raise ValueError('Для C28x нужен непустой диапазон с чётным числом байтов.')
if len(data) > 262144:
raise ValueError('Выберите диапазон не более 256 КиБ.')
directory = Path(tools)
for executable in ('cl2000.exe', 'dis2000.exe'):
if not (directory / executable).is_file():
raise ValueError(f'Не найден {executable} в {directory}')
with tempfile.TemporaryDirectory(prefix='setgui-disasm-') as folder:
root = Path(folder)
source = root / 'dump.asm'
source.write_text(' .text\n' + '\n'.join(
f' .word 0x{int.from_bytes(data[i:i + 2], "little"):04X}'
for i in range(0, len(data), 2)) + '\n', encoding='ascii')
def run(arguments):
result = subprocess.run(arguments, cwd=root, capture_output=True,
text=True, errors='replace', timeout=30,
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
if result.returncode:
raise RuntimeError(result.stderr or result.stdout or 'Ошибка TI tools')
return result.stdout
run([str(directory / 'cl2000.exe'), '--silicon_version=28', '--compile_only',
'--output_file=dump.obj', str(source)])
return run([str(directory / 'dis2000.exe'), '--data_as_text', 'dump.obj'])

View File

@@ -0,0 +1,38 @@
// Local C pseudocode export for a raw, user-selected C28x code range.
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public class SetguiExportC extends GhidraScript {
public void run() throws Exception {
Address entry = currentProgram.getMinAddress();
disassemble(entry);
createFunction(entry, null);
analyzeAll(currentProgram);
DecompInterface decompiler = new DecompInterface();
try (PrintWriter out = new PrintWriter(getScriptArgs()[0], StandardCharsets.UTF_8.name())) {
out.println("/* Ghidra C pseudocode. Recovered types/names may be incomplete. */");
if (!decompiler.openProgram(currentProgram))
throw new Exception(decompiler.getLastMessage());
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
int count = 0;
while (functions.hasNext() && !monitor.isCancelled()) {
Function function = functions.next();
if (function.isExternal()) continue;
DecompileResults result = decompiler.decompileFunction(function, 10, monitor);
out.println("/* Address: " + function.getEntryPoint() + " */");
if (result.decompileCompleted() && result.getDecompiledFunction() != null) {
out.println(result.getDecompiledFunction().getC());
count++;
} else out.println("/* Decompilation failed: " + result.getErrorMessage() + " */");
}
if (count == 0) throw new Exception("No functions decompiled. Select a range beginning at a function entry.");
out.println("/* SETGUI_DECOMPILE_SUCCESS */");
} finally { decompiler.dispose(); }
}
}

View File

@@ -0,0 +1,40 @@
"""Portable, read-only TMS firmware verification against an immutable BIN."""
from protocan.tms2812 import build_upload_request, decode_upload_reply
from .bin_compare import compare_sha256
class TmsFirmwareCheck:
def __init__(self, controller, word_address, image, block_size=256):
self.image = bytes(image)
if not 1 <= controller <= 255:
raise ValueError("Для проверки нужен адрес контроллера 1..255")
if not self.image or len(self.image) > 4 * 1024 * 1024:
raise ValueError("Размер BIN должен быть 1..4194304 байт")
if not 0 <= word_address <= 0xFFFFFFFF or word_address + (len(self.image) + 1) // 2 > 0x100000000:
raise ValueError("Диапазон памяти вне 32-битного адресного пространства")
if not 2 <= block_size <= 65534 or block_size % 2:
raise ValueError("Размер блока должен быть чётным: 2..65534 байт")
self.controller, self.word_address = controller, word_address
self.block_size = block_size
self.readback = bytearray()
@property
def done(self):
return len(self.readback) == len(self.image)
@property
def count(self):
return min(self.block_size, len(self.image) - len(self.readback))
def request(self):
if self.done:
raise ValueError("Проверка уже завершена")
return build_upload_request(self.controller, self.word_address + len(self.readback) // 2, self.count)
def accept(self, reply):
if self.done:
raise ValueError("Проверка уже завершена")
self.readback.extend(decode_upload_reply(reply, self.controller, self.count))
def result(self):
return compare_sha256(self.image, bytes(self.readback))

View File

@@ -0,0 +1,110 @@
"""Firmware-info v1 and the adjacent release passport (no device I/O)."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
from protocan.periph28335 import with_crc
MAX_IMAGE_SIZE = 199993
REGISTER_ADDRESS = 0x1200
REGISTER_COUNT = 12
def read_request(controller: int) -> bytes:
if not 1 <= controller <= 255:
raise ValueError("Для чтения версии нужен адрес контроллера 1..255")
return with_crc(bytes((controller, 3)) + REGISTER_ADDRESS.to_bytes(2, 'big')
+ REGISTER_COUNT.to_bytes(2, 'big'))
@dataclass(frozen=True)
class FirmwareIdentity:
version: tuple[int, int, int]
built_at: datetime
build_id: str
def __post_init__(self):
if (len(self.version) != 3 or any(type(v) is not int or not 0 <= v <= limit
for v, limit in zip(self.version, (255, 255, 999)))
or not 2000 <= self.built_at.year <= 2199
or self.built_at.utcoffset() is None
or self.built_at.microsecond
or len(self.build_id) != 8
or any(not 32 <= ord(c) <= 126 for c in self.build_id)):
raise ValueError("Некорректные данные версии прошивки")
def describe(self):
version = '.'.join(map(str, self.version))
return f"{version} · {self.built_at:%Y-%m-%d %H:%M:%S} UTC · Build ID {self.build_id}"
def decode_identity(words):
if len(words) != REGISTER_COUNT or words[0] != 1:
raise ValueError("Неизвестный формат версии (требуется firmware-info v1)")
if any(type(w) is not int or not 0 <= w <= 65535 for w in words):
raise ValueError("Некорректные регистры версии")
try:
stamp = datetime(words[4], words[5] >> 8, words[5] & 255,
words[6] >> 8, words[6] & 255, words[7], tzinfo=timezone.utc)
build_id = bytes(b for w in words[8:] for b in (w >> 8, w & 255)).decode('ascii')
return FirmwareIdentity(tuple(words[1:4]), stamp, build_id)
except (ValueError, UnicodeError) as error:
raise ValueError("Некорректные данные версии: " + str(error)) from error
def inspect_image(path: Path, data: bytes | None = None, *, expected_eeprom=None,
expected_product="Balsam-167-PM35", expected_target="TMS320F28335",
max_image_size=MAX_IMAGE_SIZE):
"""Return None for legacy images; reject a present but invalid passport.
The caller may supply the exact bytes it will upload, avoiding a second
read between hash verification and transmission.
"""
if data is None:
with path.open('rb') as stream:
data = stream.read(max_image_size + 1)
if not 0 < len(data) <= max_image_size:
raise ValueError(f"Размер бинарного файла должен быть 1..{max_image_size} байт")
passport = Path(str(path) + '.manifest.json')
if not passport.exists():
return None
try:
with passport.open('r', encoding='utf-8-sig') as stream:
raw = stream.read(65537)
if len(raw) > 65536:
raise ValueError("Слишком большой паспорт прошивки")
meta = json.loads(raw)
if (meta['schema'] != 1 or meta['identity_contract'] != 1
or meta['identity_register_count'] != REGISTER_COUNT):
raise ValueError("Неизвестный формат паспорта")
if meta['product'] != expected_product or meta['target'] != expected_target:
raise ValueError(f"Паспорт предназначен для другого устройства (ожидается {expected_product} / {expected_target})")
if (type(meta['size_bytes']) is not int or meta['size_bytes'] != len(data)
or meta['sha256'] != hashlib.sha256(data).hexdigest()):
raise ValueError("BIN не соответствует паспорту: размер или SHA-256 не совпадает")
if expected_eeprom is not None and meta.get('eeprom') is not None:
if meta['eeprom'] != expected_eeprom:
raise ValueError(f"BIN для {meta['eeprom']}, а выбрана EEPROM {expected_eeprom}")
stamp = datetime.fromisoformat(meta['built_at_utc'].replace('Z', '+00:00'))
if stamp.utcoffset() is None or stamp.utcoffset().total_seconds() != 0:
raise ValueError("Дата паспорта должна быть в UTC")
return FirmwareIdentity(tuple(meta['version']), stamp, meta['build_id'])
except (KeyError, TypeError, ValueError, AttributeError) as error:
raise ValueError("Ошибка паспорта: " + str(error)) from error
def compare_identity(device, image):
if device is None or image is None:
return "Сравнение недоступно: нужны версия с платы и проверенный паспорт BIN."
if device == image:
return "Версия, дата сборки и Build ID совпадают. Содержимое памяти платы этим не проверяется."
if image.version > device.version:
return "Выбранная версия новее установленной."
if image.version < device.version:
return "Выбранная версия старее установленной (понижение версии)."
return "Номер версии совпадает, но дата сборки или Build ID отличаются."

View File

@@ -8,6 +8,8 @@ See docs/tektronix-dpo4034.md for wiring, commands and scaling.
"""
from dataclasses import dataclass
import math
import struct
import sys
def resource_name(interface, address, board=0):
@@ -52,18 +54,31 @@ class DPO4034:
try:
import pyvisa
except ImportError as exc:
raise RuntimeError("Установите PyVISA и NI-VISA или TekVISA") from exc
if getattr(sys, "frozen", False):
message = "В сборке SETGUI недоступна PyVISA. Используйте исправленную сборку EXE."
else:
message = "В Python-окружении недоступна PyVISA. Установите PyVISA (для Python 3.8: версия 1.14.1)."
raise RuntimeError(message + " Причина: " + str(exc)) from exc
factory = pyvisa.ResourceManager
try:
self.manager = factory()
try:
self.manager = factory()
except (OSError, ValueError) as exc:
if self._factory is not None:
raise
bits = struct.calcsize("P") * 8
raise RuntimeError(
f"Не удалось загрузить VISA-драйвер. Установите NI-VISA или TekVISA {bits}-bit "
"с поддержкой вашей Windows (для Win7 нужна совместимая версия). Причина: " + str(exc)
) from exc
self.instrument = self.manager.open_resource(resource, open_timeout=timeout_ms)
self.instrument.timeout = timeout_ms
self.instrument.write_termination = "\n"
self.instrument.read_termination = "\n"
identity = self.instrument.query("*IDN?").strip()
fields = identity.upper().split(",")
if len(fields) < 2 or "TEKTRONIX" not in fields[0] or fields[1].strip() not in ("DPO4034", "MDO4034C"):
raise ValueError(f"Ожидался Tektronix DPO4034 или MDO4034C, получено: {identity}")
if len(fields) < 2 or "TEKTRONIX" not in fields[0] or fields[1].strip() not in ("DPO4034", "DPO4034B", "MDO4034C"):
raise ValueError(f"Ожидался Tektronix DPO4034, DPO4034B или MDO4034C, получено: {identity}")
self.instrument.write("HEADER OFF")
return identity
except Exception:

View File

@@ -54,6 +54,8 @@ class ScopeSession:
value = CaptureFrame(value, prepare_preview(value, args[3]))
if automatic:
state.consumed(status)
elif action == "sync_settings":
value = client.settings(*args)
elif action == "export":
filename, waves = args
with open(filename, "w", newline="", encoding="utf-8-sig") as stream:

View File

@@ -14,6 +14,7 @@ from dataclasses import dataclass
from protocan import periph28335 as protocol
BASE = 0x1000
PROTECTION = 0x1300 # Balsam 167: versioned voltage protection/test block.
DATA = 0x1010
LIVE = 0x1100
EMU_CONTROL = 0x1200
@@ -79,7 +80,7 @@ def request(controller: int, function: int, address: int, value: int, *, record_
raise ValueError('Неверный размер записи логгера')
capacity = {40: 800, 42: 760}[record_words]
if function == 3:
valid = ((address == BASE and value == 16) or
valid = ((address == PROTECTION and value == 8) or (address == BASE and value == 16) or
(address == DATA and 1 <= value <= 120 and (value % WORDS == 0 or value in (42, 84))) or (address == LIVE and value == 124) or (address == EMU_CONTROL and value == 4)
or (address == FAST_CAPABILITY and value == 4)
or (FAST_DATA <= address < FAST_DATA + capacity * record_words
@@ -87,7 +88,9 @@ def request(controller: int, function: int, address: int, value: int, *, record_
and 1 <= value <= 120 and value % record_words == 0
and address + value <= FAST_DATA + capacity * record_words))
else:
valid = function == 6 and (address in (BASE, BASE + 1, BASE + 2, BASE + 3) or
valid = function == 6 and ((address == PROTECTION + 2 and value in (0, 1, 2, 3)) or
(address == PROTECTION + 3 and 1 <= value <= 60000) or
address in (BASE, BASE + 1, BASE + 2, BASE + 3) or
(address == EMU_CONTROL + 2 and value in (0, 1)))
if not valid:
raise ValueError('Недопустимый запрос логгера УМП')

View File

@@ -11,7 +11,7 @@
"""
from dataclasses import dataclass, field
import struct
from set_devices.ump_logger import BASE, DATA, LIVE, EMU_CONTROL
from set_devices.ump_logger import BASE, DATA, LIVE, EMU_CONTROL, PROTECTION
MARKER = 0x1F00
MAX_WORDS = 124
@@ -26,10 +26,12 @@ def request_ids(mode):
def build_request(mode, token, function, address, value):
if not 1 <= token <= 255:
raise ValueError('Неверный номер CAN-запроса')
valid = ((function == 3 and ((address == BASE and value == 16) or
valid = ((function == 3 and ((address == PROTECTION and value == 8) or (address == BASE and value == 16) or
(address == DATA and value in (40, 80, 120, 42, 84)) or
(address == LIVE and value == MAX_WORDS) or (address == EMU_CONTROL and value == 4))) or
(function == 6 and ((BASE <= address <= BASE+3 and 0 <= value <= 65535) or
(address == PROTECTION+2 and value in (0, 1, 2, 3)) or
(address == PROTECTION+3 and 1 <= value <= 60000) or
(address == EMU_CONTROL+2 and value in (0, 1)))))
if not valid:
raise ValueError('Недопустимый запрос CAN-логгера')

View File

@@ -24,7 +24,8 @@ def sample(time_ms, period_ms=20, *, repeat=True):
else:
current = 4
words = [0] * codec.WORDS
# Extended logger record includes both DC-link voltage measurements.
words = [0] * 42
words[0], words[1] = time_ms & 65535, (time_ms >> 16) & 65535
words[2] = 1 | (0x100 if 5000 <= phase < 5000 + period_ms else 0)
words[3] = (int(started) << 7) | (int(reset) << 6)
@@ -42,6 +43,14 @@ def sample(time_ms, period_ms=20, *, repeat=True):
words[18:24] = [0xE07F, words[3], 6, 16, (time_ms // 100) & 65535, 0]
words[24:31] = [6, 16, 500, 3500, 2000, 500, 3500]
words[31:36] = [ramp if started else 0, 2500, 60, 160, round(ramp * 1000 / 2500)]
if started:
charge = min(1.0, (phase - 1000) / 1500)
elif phase >= 5000:
charge = max(0.0, 1 - (phase - 5000) / 800)
else:
charge = 0.0
words[40] = round(charge * (600 + 3 * math.sin(phase / 80)))
words[41] = round(charge * (590 + 4 * math.sin(phase / 95 + .7)))
return codec.sample(words)

View File

@@ -3,11 +3,17 @@ import os
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtWidgets import QApplication
from PySide6.QtTest import QTest
from PySide6.QtCore import QEventLoop, QTimer
from pysetterminal.rawprotocol import RawProtocolWidget
from pysetterminal.serialbus import SerialBus, SerialTab
def process_events(milliseconds):
loop = QEventLoop()
QTimer.singleShot(milliseconds, loop.quit)
loop.exec()
class PendingProtocol:
def __init__(self):
self.requests = []
@@ -39,11 +45,11 @@ class MacroTests(unittest.TestCase):
def test_scheduled_execution_returns_and_can_be_cancelled(self):
self.tab.execute_macro_commands([self.command])
self.assertEqual(self.protocol.requests, [])
QTest.qWait(20)
process_events(20)
self.assertEqual(self.protocol.requests, [(0x200B, 0x35)])
self.assertTrue(self.tab.stop_macro_btn.isEnabled())
self.tab.stop_macro()
QTest.qWait(20)
process_events(20)
self.assertFalse(self.tab._macro_running)
self.assertTrue(self.tab.bus_read_btn.isEnabled())
self.assertEqual(len(self.protocol.requests), 1)
@@ -52,20 +58,20 @@ class MacroTests(unittest.TestCase):
self.tab.execute_macro_commands([self.command])
self.tab.stop_macro()
self.tab.execute_macro_commands([self.command])
QTest.qWait(20)
process_events(20)
self.assertEqual(len(self.protocol.requests), 1)
self.protocol.callback(False, "CRC error")
QTest.qWait(20)
process_events(20)
self.assertFalse(self.tab._macro_running)
self.assertEqual(len(self.protocol.requests), 1)
def test_completion_runs_next_step_and_updates_value(self):
self.tab.execute_macro_commands([self.command])
QTest.qWait(20)
process_events(20)
self.protocol.callback(True, {'raw': b"ack"})
QTest.qWait(20)
process_events(20)
self.assertEqual(self.protocol.requests[-1], (0x200F, None))
self.protocol.callback(True, {'value': 0x1234, 'raw_response': {'raw': b"reply"}})
QTest.qWait(20)
process_events(20)
self.assertFalse(self.tab._macro_running)
self.assertEqual(self.tab.bus_read_value_label.text(), "0x1234 (4660)")